Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
166 commits
Select commit Hold shift + click to select a range
d91083b
fix(website-policy): key blocklist cache on the real default config path
aydnOktay Jul 4, 2026
a0c90ed
fix(skills): retry rate-limited Contents API directory listings
teknium1 Jul 4, 2026
9e872db
fix(redact): skip env-assignment redaction for programmatic env lookups
teknium1 Jul 4, 2026
316e775
fix(vision): unified image-source resolver + terminal-backend confine…
jquesnelle Jul 3, 2026
ab2f5a0
fix(vision): address review — restore bare relative paths, remove dea…
teknium1 Jul 4, 2026
ac94f2c
fix(vision): convert SVG/unsupported image formats to PNG before embe…
JAlmanzarMint Jun 25, 2026
4ac8c75
fix(vision): mkdir converted-PNG output dir; wire SVG pass-through to…
teknium1 Jul 4, 2026
6c06835
fix(vision): stdin=DEVNULL on rasterizer subprocess (stdin guard)
teknium1 Jul 4, 2026
0d27d2e
fix(vision): bound the sandbox exec-read at the ingest cap
teknium1 Jul 4, 2026
ec29590
fix(webhook): enforce body-size limit on chunked requests
Jul 4, 2026
2b4ec00
fix(api_server): return 413 for oversized chunked bodies
Jul 4, 2026
ddd3a2d
fix(auxiliary): fall back to token resolver when anthropic pool has n…
Jigoooo Jul 4, 2026
2bb11ad
fix: classify OpenRouter 'no tool use' 404 as model_not_found with fa…
webtecnica Jul 4, 2026
55e7986
fix(poolside): handle integer finish_reason and tool_call id
Jul 4, 2026
70dffb6
fix(codex): recover final app-server text without completion
ooiuuii Jul 4, 2026
8552cac
fix: drop unrelated package-lock churn and dead poolside picker entry
teknium1 Jul 4, 2026
10f7cb0
chore(release): AUTHOR_MAP entries for salvaged PR authors
teknium1 Jul 4, 2026
ff4c817
fix(gateway): attach credential_pool to session /model overrides
tuancookiez-hub Jul 4, 2026
11b4a21
fix(gateway): clear last-resolved-model cache on /new and compression…
liuhao1024 Jul 4, 2026
7e8f50a
fix(gateway): load display config from routed profile
tianma-if Jul 4, 2026
af01b3c
fix(config): stop provider-key warn-storm that stalls Windows logging
lEWFkRAD Jul 4, 2026
eb0cc27
fix(logging): drive rotating file handlers through an async QueueList…
lEWFkRAD Jul 4, 2026
ac68a64
fix(gateway): drain async log queue on os._exit shutdown backstop
kshitijk4poor Jul 5, 2026
1388cd1
fix(logging): thread-safe queue state + bounded hard-exit drain + rec…
kshitijk4poor Jul 5, 2026
f512d6f
feat(plugins): pre_tool_call approve action escalates to human gate
kshitijk4poor Jul 5, 2026
a0a3c71
fix(telegram): dedup saturated mid-stream overflow previews to stop f…
teknium1 Jul 5, 2026
5b85932
fix(gateway): cap proxy SSE line buffer
ooiuuii Jul 4, 2026
132bb8a
fix(yuanbao): restore active singleton after WS reconnect
liuhao1024 Jul 4, 2026
6c7960c
fix(whatsapp_cloud): honor documented WHATSAPP_CLOUD_ALLOWED_USERS / …
sahil-shubham Jul 4, 2026
c018096
test(whatsapp_cloud): intake-gate regression coverage for documented …
teknium1 Jul 4, 2026
d810ff2
chore(release): AUTHOR_MAP entries for salvaged PR authors
teknium1 Jul 4, 2026
485ae54
fix(gateway): pass full transcript to compressor instead of filtered …
teknium1 Jul 5, 2026
6f052b7
fix(copilot): set x-initiator per turn so user prompts bill as premiu…
teknium1 Jul 5, 2026
f23026f
Merge pull request #58536 from NousResearch/salvage/3955-webhook-chun…
teknium1 Jul 5, 2026
4eaf5ba
Merge pull request #58534 from NousResearch/salvage/2854-redact-geten…
teknium1 Jul 5, 2026
d51657c
Merge pull request #58531 from NousResearch/salvage/3033-contents-api…
teknium1 Jul 5, 2026
b3c7b34
Merge pull request #58526 from NousResearch/salvage/3923-website-poli…
teknium1 Jul 5, 2026
9ae17b8
security(vision): route local-file inputs through the shared credenti…
srojk34 Jul 3, 2026
b6b9bcd
fix(profiles): preserve symlinks during profile export
liuhao1024 Jul 4, 2026
8d9684c
fix(profiles): allowlist default-export paths + preserve symlinks (#5…
Jul 4, 2026
b7192b1
fix(profiles): preserve symlinks in clone-all and skills clone paths
mvanhorn Jul 4, 2026
020a716
chore(release): AUTHOR_MAP entries for salvaged PR authors
teknium1 Jul 4, 2026
fc18d15
fix: preserve static custom provider models
lord-dubious Jul 4, 2026
7915837
fix(auth): prune stale custom model credentials
tianma-if Jul 4, 2026
e4da3a7
chore(release): AUTHOR_MAP entry for salvaged PR author
teknium1 Jul 5, 2026
ce82b0c
fix: `hermes journey` crashes on Windows due to `%-d` strftime directive
wyuebei-cloud Jul 1, 2026
7e037e1
fix: cover remaining GNU-only %-d strftime site in learning graph render
teknium1 Jul 4, 2026
dec4485
chore(release): AUTHOR_MAP entries for salvaged PR authors
teknium1 Jul 4, 2026
70449a4
fix(security): add timestamp-bound V2 signature for generic webhook r…
MorAlekss Jul 4, 2026
708b57e
fix(webhook): rate-limit V1 deprecation warning + document V2 signature
teknium1 Jul 4, 2026
1b7853d
fix(gateway): add system dirs to PATH for UV Python compatibility
kevinrajaram Mar 30, 2026
619db01
fix(gateway): move PATH bootstrap below imports, gate to POSIX
teknium1 Jul 4, 2026
4751af0
feat(errors): fail fast on TLS certificate verification failures with…
teknium1 Jul 5, 2026
edf8e0b
feat(mcp): surface MCP server log notifications in agent.log (#57416)
teknium1 Jul 5, 2026
3047996
fix(gateway): tolerate punctuation on silence markers
ooiuuii Jul 4, 2026
9767e19
feat(skills): stacked slash-skill invocations — /skill-a /skill-b do …
teknium1 Jul 5, 2026
cb6c47a
feat(approvals): /deny <reason> relays denial reason to the agent (po…
teknium1 Jul 5, 2026
ebfc49c
fix(approval): require exact ./.. segments in the root-collapse hardl…
teknium1 Jul 5, 2026
d577408
fix(webhook): reject generic V2 signature missing timestamp instead o…
MorAlekss Jul 4, 2026
e02cef0
fix(memory): guard local uploads against credential reads
necoweb3 Jul 3, 2026
8324dd1
fix(agent): replace custom socket_options transport with httpx pool-l…
DavidMetcalfe Jun 29, 2026
51c1ba6
fix(agent): apply pool-level keepalive to the process_bootstrap sibli…
teknium1 Jul 5, 2026
c13281a
Guard native image routing with file safety
necoweb3 Jul 4, 2026
d8b5126
fix(update): skip cua-driver refresh when Applications is unwritable
Jun 17, 2026
d537d29
fix(computer-use): increase cua-driver session startup timeout from 1…
liuhao1024 Jul 2, 2026
7fde19a
fix(cli): unwedge cua-driver installer timeouts — group-kill, stale-l…
teknium1 Jul 5, 2026
1c15673
docs: warn that mid-session model switches break prompt caching (#58747)
teknium1 Jul 5, 2026
2c0820c
feat(cli): autocomplete + ghost text for stacked slash-skill invocati…
teknium1 Jul 5, 2026
24a7546
fix(cli): drop shell=True from cua-driver installer — download to mks…
teknium1 Jul 5, 2026
de4310c
fix(computer-use): report the wedged startup phase in the session rea…
teknium1 Jul 5, 2026
7af9abd
fix(computer_use): fall back to CLI transport when cua-driver MCP bri…
Jun 7, 2026
13b75e7
fix(computer_use): re-fetch via CLI when MCP returns silent-empty cap…
Jun 7, 2026
519ec7b
fix(computer_use): parse (label) and = "value" AX element label forms
Jun 7, 2026
95fc3c6
chore: add alastraz to AUTHOR_MAP for PR #41383 salvage
teknium1 Jul 5, 2026
bfc5262
feat: add STT transcript echo toggle
devatnull Jun 28, 2026
406eb71
fix: gate interrupt STT transcript echoes
devatnull Jun 28, 2026
4be749d
fix: honor top-level STT transcript echo config
devatnull Jul 5, 2026
5580013
feat(desktop,docs): surface stt.echo_transcripts in desktop settings …
vKongv Jul 5, 2026
0ca2a92
chore: add devatnull to AUTHOR_MAP for PR #58697 salvage
teknium1 Jul 5, 2026
11627fd
feat(whatsapp): native Baileys polls, clarify-as-poll, locations, and…
devatnull Jul 5, 2026
b0f2bdb
fix(whatsapp): gate poll-vote events to Hermes-created polls + salvag…
teknium1 Jul 5, 2026
4bf5b56
feat: add generic gateway status phrases
devatnull Jun 28, 2026
fddc95f
chore: limit generic status phrases to long-running notifications
devatnull Jun 28, 2026
46fbd73
fix: strip tool progress display modes
devatnull Jun 29, 2026
12f03b1
feat: make busy steer ack configurable
devatnull Jun 28, 2026
d111faa
fix: preserve busy steer env override
devatnull Jun 29, 2026
b9de704
fix: preserve log tool-progress mode with status phrases
devatnull Jul 5, 2026
14c91ad
fix: normalize display boolean strings
devatnull Jul 5, 2026
372c0b5
chore: add devatnull to AUTHOR_MAP for PR #58700 salvage
teknium1 Jul 5, 2026
ea125dd
fix: keep Codex commentary phase out of user-visible text
devatnull Jun 28, 2026
538173f
fix(codex): route commentary-phase preamble text to reasoning channel…
annguyenNous Jun 7, 2026
b3b1e58
fix(codex): stream commentary deltas through the reasoning channel
teknium1 Jul 5, 2026
8a04b51
Port from cline/cline#11803: recursively normalize JSON-string tool a…
teknium1 Jul 5, 2026
605727e
feat(discord): optional admin-only gate for exec-approval buttons (#5…
teknium1 Jul 5, 2026
24add1d
fix(compressor): keep a user turn when compression would drop the las…
HexLab98 Jul 5, 2026
10ced05
test(compressor): pin the zero-user-turn compaction guard (#58753)
HexLab98 Jul 5, 2026
b2c5558
test(compressor): drop source-string guardrail tests
kshitijk4poor Jul 5, 2026
abf9638
Merge pull request #58974 from kshitijk4poor/salvage/compressor-zero-…
kshitijk4poor Jul 5, 2026
368e5f1
Merge pull request #58698 from kshitijk4poor/feat/pre-tool-call-appro…
kshitijk4poor Jul 5, 2026
01ee312
fix(telegram): forward keepalive limits into fallback transport
liuhao1024 Jul 5, 2026
5b04a02
docs(telegram): clarify fallback-branch limits wiring vs siblings
kshitijk4poor Jul 5, 2026
b109ade
fix(config): refuse unreadable config overwrites
ooiuuii Jul 5, 2026
123c6f3
fix(config): close unreadable-overwrite bug class at a single chokepoint
kshitijk4poor Jul 5, 2026
beaa1a0
fix(config): guard xai migration writer + drop gratuitous annotation
kshitijk4poor Jul 5, 2026
dcd70c5
fix(gateway): drain in-flight cron delivery on restart instead of dro…
HexLab98 Jul 5, 2026
6b14be0
test(gateway): cover cron-delivery drain on restart
HexLab98 Jul 5, 2026
18058c4
fix(gateway): drain housekeeping thread over its own 30s future on sh…
kshitijk4poor Jul 5, 2026
8aab8be
fix(cron): skip delivery/dispatch when the interpreter is shutting down
HexLab98 Jul 5, 2026
6d9eff2
test(cron): cover the interpreter-shutdown scheduling guard (#58720)
HexLab98 Jul 5, 2026
5986cdd
fix(cron): deliver before tearing down the agent's async clients (#58…
kshitijk4poor Jul 5, 2026
e01f58f
feat(mcp): adopt mcp__server__tool naming convention
teknium1 Jun 26, 2026
3d02761
test: update MCP parallel-batch fixture names to mcp__server__tool co…
teknium1 Jul 5, 2026
55e3ee1
fix: remove dead f-string prefixes via ruff F541 (216 sites) (#52336)
teknium1 Jul 5, 2026
ba31699
chore(providers): remove dead cloudcode-pa quota-fallback branches (#…
teknium1 Jul 5, 2026
a6079dd
feat(providers): GLM-5.2 native reasoning_effort controls (#58884)
teknium1 Jul 5, 2026
77700a0
fix(feishu): send WebSocket CLOSE frame on disconnect (#10202)
teknium1 Apr 27, 2026
1b69ad0
fix: update salvaged tests to relocated feishu adapter path
teknium1 Jul 5, 2026
eab208d
feat(hooks): spill oversized hook-injected context to disk (#20468)
teknium1 Jul 5, 2026
5a5e7e2
fix(nix): follow root pyproject inputs
WadydX Jun 3, 2026
9d2ff58
fix(yuanbao): skip resource resolve on cache hits
heathley May 29, 2026
04d732d
fix(gateway): re-check every stacked skill against the platform-disab…
srojk34 Jul 5, 2026
f10851e
fix(computer-use): sanitize subprocess env in cua-driver CLI fallback…
srojk34 Jul 5, 2026
1e2914b
fix(telegram): redact bot token from connect/disconnect/send_document…
srojk34 Jul 5, 2026
3817ff1
security(raft): enforce body-size limit on chunked requests
srojk34 Jul 5, 2026
cdcbc3a
fix(gateway): clear last-resolved-model cache on 3 more conversation-…
srojk34 Jul 5, 2026
2e2212b
fix(discord): dedup saturated mid-stream overflow previews to stop ed…
srojk34 Jul 5, 2026
a573066
fix(redact): skip env-lookup exception for JSON/YAML config field red…
srojk34 Jul 5, 2026
0b67ff2
fix(agents): bound streaming error-response body reads
teknium1 Jun 22, 2026
747386e
refactor: consolidate gateway session metadata into state.db (#58899)
teknium1 Jul 5, 2026
74cc9ee
Revert "Merge pull request #58698 from kshitijk4poor/feat/pre-tool-ca…
kshitijk4poor Jul 5, 2026
c9a150d
Merge pull request #59131 from kshitijk4poor/revert/58698-pre-tool-ap…
kshitijk4poor Jul 5, 2026
e3203e4
fix(config): invalidate load_config cache when referenced ${VAR} env …
falkoro Jul 4, 2026
e985e34
chore: add falkoro to AUTHOR_MAP
teknium1 Jul 5, 2026
9ad912a
fix(agent): honor auxiliary.<task>.base_url/api_key when provider is …
falkoro Jul 4, 2026
cd2b360
feat: add Docker terminal network toggle
teknium1 Jun 15, 2026
3167dba
fix(docker): widen docker_network to file/code-exec paths + guard con…
teknium1 Jul 5, 2026
1197d2b
fix(mattermost): accept leading-space slash commands
May 25, 2026
2f2e608
chore: add l0h1nth to AUTHOR_MAP for PR #32210 salvage
teknium1 Jul 5, 2026
25f0cec
Port from nearai/ironclaw#5029: graceful char-budget truncation for r…
teknium1 Jun 22, 2026
f514132
fix: disclose mid-line clamp in truncation hint
teknium1 Jul 5, 2026
6511767
fix(gateway): apply platform-disabled skill gate to bundle invocation…
teknium1 Jul 5, 2026
0823230
fix(computer-use): sanitize env on the 4 remaining cua-driver spawn s…
teknium1 Jul 5, 2026
8986981
security(gateway): set explicit client_max_size on 3 uncapped aiohttp…
teknium1 Jul 5, 2026
e2fe529
feat(approvals): user-defined deny rules that block commands even und…
teknium1 Jul 5, 2026
de7e0a8
fix(docker): heal pairing-dir ownership after `docker exec` writes (#…
teknium1 Jul 5, 2026
1f2a33f
fix(mcp): gate probe prompts/resources on config + advertised capabil…
HexLab98 Jul 5, 2026
c9adbaf
test(mcp): cover probe capability + config gating for prompts/resources
HexLab98 Jul 5, 2026
b57fe5c
fix(setup): exclude posture toolsets from blank-slate disabled_toolsets
liuhao1024 Jul 2, 2026
e5636da
fix(toolsets): preserve core tools when a posture toolset is in disab…
bbopen Jul 3, 2026
a05b64d
test(setup): blank-slate disabled list must not overlap kept tools
HexLab98 Jul 5, 2026
6fad6f1
fix(whatsapp): contain and surface inbound media download failures (p…
teknium1 Jul 6, 2026
8e09afd
fix(auxiliary): inherit model.api_key for custom endpoint when per-ta…
Tranquil-Flow Jun 30, 2026
ede7e31
fix(auxiliary): gate main api_key inheritance on same-host aux base_url
teknium1 Jul 5, 2026
3cd93f6
fix(photon): auto-reinstall stale sidecar deps before start
Jul 3, 2026
127d2ee
fix(photon): bound the sidecar dep self-heal npm run with a timeout
teknium1 Jul 5, 2026
c5a8df3
chore(release): map jashlee+microsoft@microsoft.com -> s905060 (PR #5…
teknium1 Jul 5, 2026
940b69b
fix(sms): bound Twilio webhook body reads to prevent OOM
Alix-007 Jun 29, 2026
3dd5ce2
fix(sms): set client_max_size on the Twilio webhook Application
teknium1 Jul 5, 2026
4f4cbff
fix(msgraph): enforce webhook body limits
binhnt92 May 14, 2026
deae37e
fix(tests): add missing json import in msgraph webhook test fixture
teknium1 Jul 5, 2026
eec92a9
Enforce WhatsApp Cloud webhook body limit while reading
ooiuuii Jun 29, 2026
e82d71d
fix(whatsapp): set client_max_size on the webhook Application
teknium1 Jul 5, 2026
a26680e
Enforce Feishu webhook body limit while reading
ooiuuii Jun 29, 2026
2bcb893
fix(feishu): set client_max_size on the webhook Application
teknium1 Jul 5, 2026
6133285
chore(release): map Alix-007 author email for PR #54620 salvage
teknium1 Jul 5, 2026
6865645
feat(computer-use): draw self-localization action marker on follow-up…
trac3r00 Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ graft locales
# built from the sdist (e.g. Homebrew, downstream packagers). package-data
# below covers the wheel; this covers the sdist. See #34034 / #28149.
recursive-include plugins plugin.yaml plugin.yml
# Gateway assets include images plus YAML catalogs such as status_phrases.yaml.
recursive-include gateway/assets *
global-exclude __pycache__
global-exclude *.py[cod]
2 changes: 1 addition & 1 deletion acp_adapter/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ def _format_session_search_result(result: Optional[str]) -> Optional[str]:
return None
mode = data.get("mode") or "search"
query = data.get("query")
lines = ["Recent sessions" if mode == "recent" else f"Session search results" + (f" for `{query}`" if query else "")]
lines = ["Recent sessions" if mode == "recent" else "Session search results" + (f" for `{query}`" if query else "")]
if not results:
lines.append(str(data.get("message") or "No matching sessions found."))
return "\n".join(lines)
Expand Down
2 changes: 2 additions & 0 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,8 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:
working_dir=os.getenv("TERMINAL_CWD") or None,
)
agent._user_turn_count = 0
# Copilot x-initiator flag: first API call of a user turn sends "user" (#3040).
agent._is_user_initiated_turn = False

# Cumulative token usage for the session
agent.session_prompt_tokens = 0
Expand Down
97 changes: 94 additions & 3 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,76 @@ def _read_main_provider() -> str:
return ""


def _read_main_api_key() -> str:
"""Read the user's main model API key from the runtime override or config.

Mirrors ``_read_main_model`` / ``_read_main_provider``: checks the
process-local ``_RUNTIME_MAIN_API_KEY`` override first (set by
``set_runtime_main`` when an AIAgent is active), then falls back to
``model.api_key`` in config.yaml.

Used by the ``custom`` provider fallback chain so that auxiliary tasks
configured with an explicit ``base_url`` but empty ``api_key`` inherit
the main model's credentials instead of falling to ``no-key-required``
(issue #9318).
"""
override = _RUNTIME_MAIN_API_KEY
if isinstance(override, str) and override.strip():
return override.strip()
try:
from hermes_cli.config import load_config
cfg = load_config()
model_cfg = cfg.get("model", {})
if isinstance(model_cfg, dict):
key = model_cfg.get("api_key", "")
if isinstance(key, str) and key.strip():
return key.strip()
except Exception:
pass
return ""


def _read_main_base_url() -> str:
"""Read the main model's base_url from the runtime override or config.

Same override-then-config pattern as ``_read_main_api_key``.
"""
override = _RUNTIME_MAIN_BASE_URL
if isinstance(override, str) and override.strip():
return override.strip()
try:
from hermes_cli.config import load_config
cfg = load_config()
model_cfg = cfg.get("model", {})
if isinstance(model_cfg, dict):
base = model_cfg.get("base_url", "")
if isinstance(base, str) and base.strip():
return base.strip()
except Exception:
pass
return ""


def _read_main_api_key_if_same_host(aux_base_url: str) -> str:
"""Return the main api_key only when *aux_base_url* points at the same
host as the main model's base_url.

The #9318 use case is an auxiliary task sharing the main model's
self-hosted gateway (same host, different model) with an empty per-task
api_key. Inheriting unconditionally would send the main credential to
ANY host a misconfigured aux base_url names — a cross-host credential
leak. A host mismatch keeps the previous fail-safe behavior
(``no-key-required`` → 401).
"""
aux_host = base_url_hostname(aux_base_url)
if not aux_host:
return ""
main_host = base_url_hostname(_read_main_base_url())
if not main_host or aux_host != main_host:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Auxiliary credential inheritance can now send the main API key to a different service on the same hostname but different port. The new same-host check uses hostname-only matching, so tightening this to host+effective-port (or full origin) would avoid cross-service credential leakage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At agent/auxiliary_client.py, line 2071:

<comment>Auxiliary credential inheritance can now send the main API key to a different service on the same hostname but different port. The new same-host check uses hostname-only matching, so tightening this to host+effective-port (or full origin) would avoid cross-service credential leakage.</comment>

<file context>
@@ -2003,6 +2003,76 @@ def _read_main_provider() -> str:
+    if not aux_host:
+        return ""
+    main_host = base_url_hostname(_read_main_base_url())
+    if not main_host or aux_host != main_host:
+        return ""
+    return _read_main_api_key()
</file context>

return ""
return _read_main_api_key()


# Process-local override set by AIAgent at session/turn start. Single-threaded
# per turn — no lock needed. Cleared by ``clear_runtime_main()``.
_RUNTIME_MAIN_PROVIDER: str = ""
Expand Down Expand Up @@ -2392,11 +2462,19 @@ def _try_anthropic(explicit_api_key: str = None) -> Tuple[Optional[Any], Optiona
return None, None

pool_present, entry = _select_pool_entry("anthropic")
if pool_present:
if entry is None:
return None, None
if pool_present and entry is not None:
token = explicit_api_key or _pool_runtime_api_key(entry)
else:
# Pool absent, OR pool present but no usable entry (expired token +
# stale refresh_token, all entries exhausted, etc). Fall through to the
# legacy resolver instead of hard-failing: a temporarily dead pool
# entry must not wedge auxiliary tasks when a valid standalone
# credential (ANTHROPIC_TOKEN, credentials file, API key) exists. This
# matches the openrouter and codex paths, which already fall back to
# their env/auth-store credential on (True, None). Without this, the
# goal judge and every other Anthropic-routed side channel died with
# "no auxiliary client configured" while the main session stayed
# healthy (it resolves the env token directly).
entry = None
token = explicit_api_key or resolve_anthropic_token()
if not token:
Expand Down Expand Up @@ -4234,6 +4312,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "",
custom_key = (
(explicit_api_key or "").strip()
or os.getenv("OPENAI_API_KEY", "").strip()
or _read_main_api_key_if_same_host(custom_base)
or "no-key-required" # local servers don't need auth
)
if not custom_base:
Expand Down Expand Up @@ -5524,6 +5603,18 @@ def _preserve_provider_with_base_url(prov: Optional[str]) -> bool:
if cfg_provider:
cfg_provider, cfg_base_url = _expand_direct_api_alias(cfg_provider, cfg_base_url)

# An explicit provider arg without an explicit base_url must not bypass
# the task's configured endpoint: adopt auxiliary.<task>.base_url/api_key
# when the config targets the same provider (or names none), so the
# early `if provider:` return below carries the configured endpoint
# instead of falling through to main-runtime resolution (#58515).
# An explicit "auto" is excluded — it means "inherit / auto-detect" and
# must keep flowing through the existing auto-resolution chain.
if provider and provider != "auto" and not base_url and cfg_base_url and cfg_provider in (None, provider):
base_url = cfg_base_url
if not api_key:
api_key = cfg_api_key

if base_url and _preserve_provider_with_base_url(provider):
return provider, resolved_model, base_url, api_key, resolved_api_mode
if base_url:
Expand Down
148 changes: 148 additions & 0 deletions agent/bounded_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Bounded reads of HTTP error response bodies.

When a provider returns a non-OK status on a *streaming* request, Hermes reads
the response body to build a useful diagnostic error. A bare ``response.read()``
on a streaming httpx response is unbounded in two dangerous ways:

1. A server can declare (or stream) an arbitrarily large body, so the read can
balloon memory.
2. A server can open the body and then stall forever (no ``Content-Length``,
no further bytes), so the read hangs the agent indefinitely.

Both are realistic against a misbehaving proxy, a hijacked endpoint, or a
provider having a bad day. The diagnostic body is only ever shown to the user
truncated to a few hundred characters, so reading megabytes — or blocking
forever — buys nothing.

``read_streaming_error_body`` bounds the read to a byte cap and enforces a
hard wall-clock deadline, returning the decoded text snippet. Callers pass the
returned text into their existing error builders instead of touching
``response.text`` (which would be unbounded / would raise after a partial
stream read).

A subtlety the implementation must respect: ``httpx``'s ``iter_bytes()`` blocks
*inside* the C/socket read while waiting for the next chunk. A wall-clock check
placed only between yielded chunks cannot interrupt a server that opens the
body and then stalls mid-chunk — control never returns to Python until httpx's
own (often 30s+) read timeout fires. To guarantee a bounded stop regardless of
socket behavior, the read runs on a daemon worker thread and the caller waits
on it with a hard deadline; on timeout we close the response (which unblocks /
cancels the read) and return whatever partial bytes were collected.

Ported and adapted from openclaw/openclaw#95108 ("bound Anthropic error
streams"), generalized to cover Hermes's three streaming error-body sites
(native Gemini, Gemini Cloud Code, Antigravity Cloud Code).
"""

from __future__ import annotations

import logging
import threading
from typing import List, Optional

import httpx

logger = logging.getLogger(__name__)

# Defaults chosen to comfortably hold any real provider error envelope (Google
# RPC error JSON, Anthropic error JSON) while rejecting pathological bodies.
DEFAULT_ERROR_BODY_MAX_BYTES = 64 * 1024
# Hard wall-clock deadline for the whole bounded read. A streaming error body
# that does not finish within this window is abandoned and the connection is
# closed; we keep whatever partial bytes arrived.
DEFAULT_ERROR_BODY_TIMEOUT_S = 10.0


def read_streaming_error_body(
response: httpx.Response,
*,
max_bytes: int = DEFAULT_ERROR_BODY_MAX_BYTES,
timeout_s: float = DEFAULT_ERROR_BODY_TIMEOUT_S,
) -> str:
"""Read a non-OK streaming response body with a byte cap and a hard deadline.

Returns the decoded body text (UTF-8, errors replaced), truncated to
``max_bytes``. Never raises: any transport error, stall, or oversize
condition is swallowed and the best-effort partial text (or an empty
string) is returned, because this runs on the error path and must not
mask the original HTTP failure with a read error.

The byte cap protects against huge bodies; the wall-clock deadline (enforced
via a worker thread so it can interrupt a socket read that stalls mid-chunk)
protects against bodies that open and then hang.
"""
chunks: List[bytes] = []
state = {"truncated": False}
done = threading.Event()

def _drain() -> None:
total = 0
try:
for chunk in response.iter_bytes():
if not chunk:
continue
remaining = max_bytes - total
if remaining <= 0:
state["truncated"] = True
break
if len(chunk) > remaining:
chunks.append(chunk[:remaining])
total += remaining
state["truncated"] = True
break
chunks.append(chunk)
total += len(chunk)
except Exception as exc: # noqa: BLE001 - error path must not raise
logger.debug("bounded error-body read failed: %s", exc)
finally:
done.set()

worker = threading.Thread(
target=_drain, name="bounded-error-body-read", daemon=True
)
worker.start()
finished = done.wait(timeout=timeout_s)

if not finished:
logger.debug(
"bounded error-body read: hard timeout after %.1fs (%d bytes so far)",
timeout_s,
sum(len(c) for c in chunks),
)
# Closing the response cancels the in-flight socket read, letting the
# worker thread unwind. We do not join (it is a daemon and may be
# blocked in C); the partial `chunks` collected so far are returned.
_safe_close(response)
else:
_safe_close(response)

if state["truncated"]:
logger.debug(
"bounded error-body read: capped at %d bytes (max=%d)",
sum(len(c) for c in chunks),
max_bytes,
)
return b"".join(chunks).decode("utf-8", errors="replace")


def _safe_close(response: httpx.Response) -> None:
try:
response.close()
except Exception: # noqa: BLE001
pass


def read_error_body_or_default(
response: httpx.Response,
*,
max_bytes: int = DEFAULT_ERROR_BODY_MAX_BYTES,
timeout_s: float = DEFAULT_ERROR_BODY_TIMEOUT_S,
) -> Optional[str]:
"""Like ``read_streaming_error_body`` but returns ``None`` on empty body.

Convenience for callers that distinguish "no body" from "empty string".
"""
text = read_streaming_error_body(
response, max_bytes=max_bytes, timeout_s=timeout_s
)
return text or None
14 changes: 11 additions & 3 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2194,15 +2194,23 @@ def _call_chat_completions():
idx = _active_slot_by_idx[raw_idx]

if idx not in tool_calls_acc:
# Poolside may send integer id instead of string
_tc_id = tc_delta.id
if isinstance(_tc_id, int):
_tc_id = str(_tc_id)
tool_calls_acc[idx] = {
"id": tc_delta.id or "",
"id": _tc_id or "",
"type": "function",
"function": {"name": "", "arguments": ""},
"extra_content": None,
}
entry = tool_calls_acc[idx]
if tc_delta.id:
entry["id"] = tc_delta.id
if tc_delta.id is not None:
_new_id = tc_delta.id
if isinstance(_new_id, int):
_new_id = str(_new_id)
if _new_id:
entry["id"] = _new_id
if tc_delta.function:
if tc_delta.function.name:
# Use assignment, not +=. Function names are
Expand Down
21 changes: 19 additions & 2 deletions agent/codex_responses_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,15 +1166,28 @@ def _normalize_codex_response(
if item_type == "message":
item_phase = getattr(item, "phase", None)
normalized_phase = None
is_commentary_phase = False
if isinstance(item_phase, str):
normalized_phase = item_phase.strip().lower()
if normalized_phase in {"commentary", "analysis"}:
saw_commentary_phase = True
is_commentary_phase = True
elif normalized_phase in {"final_answer", "final"}:
saw_final_answer_phase = True
message_text = _extract_responses_message_text(item)
if message_text:
content_parts.append(message_text)
# Responses ``commentary``/``analysis`` phase text is mid-turn
# preamble/progress narration, never the turn's final answer
# (Codex CLI excludes it from last-message extraction; issues
# #24933 / #41293). Keep it out of assistant content so it
# can't be concatenated into — or leak as — the final response,
# but surface it through the reasoning channel so the CLI/
# gateway display it like thinking text. The exact message
# item is still preserved below for replay/cache continuity.
if is_commentary_phase:
reasoning_parts.append(message_text)
else:
content_parts.append(message_text)
raw_message_item: Dict[str, Any] = {
"type": "message",
"role": "assistant",
Expand Down Expand Up @@ -1269,7 +1282,11 @@ def _normalize_codex_response(
))

final_text = "\n".join([p for p in content_parts if p]).strip()
if not final_text and hasattr(response, "output_text"):
if (
not final_text
and hasattr(response, "output_text")
and not (saw_commentary_phase and not saw_final_answer_phase)
):
out_text = getattr(response, "output_text", "")
if isinstance(out_text, str):
final_text = out_text.strip()
Expand Down
Loading