Commit graph

2439 commits

Author SHA1 Message Date
teknium1
e82d71db40 fix(whatsapp): set client_max_size on the webhook Application
Follow-up to the salvaged #54944: before this, aiohttp's implicit 1 MiB
default client_max_size tripped BEFORE the intended 3 MB Meta cap could
apply on read() paths — the explicit value makes the documented limit
real while the bounded reader keeps chunked bodies from buffering past
3 MB (#58536/#58902/#59180 pattern).
2026-07-05 17:38:36 -07:00
luyifan
eec92a92c0 Enforce WhatsApp Cloud webhook body limit while reading 2026-07-05 17:38:36 -07:00
binhnt92
4f4cbff8bd fix(msgraph): enforce webhook body limits 2026-07-05 17:38:36 -07:00
Teknium
de7e0a8875
fix(docker): heal pairing-dir ownership after docker exec writes (#10270) (#59130)
* fix(docker): heal pairing-dir ownership after `docker exec` writes (#10270)

The official Docker image runs the gateway as the unprivileged `hermes`
user (uid 10000) via `gosu`, but `docker exec` defaults to root. Approval
files written by `docker exec <container> hermes pairing approve <code>`
end up as `-rw------- root:root`, and the post-gosu gateway process
cannot read them. The approval is silently ignored — the user keeps
hitting 'Unauthorized user' on every message.

The entrypoint's existing top-level chown is gated on the top-level
$HERMES_HOME being mis-owned, so on warm boots (where /opt/data is
already hermes:hermes) the recursive chown is skipped — meaning a
container restart does NOT self-heal the bug either.

Three-part fix:

1. docker/entrypoint.sh: chown the platforms/pairing/ (and legacy
   pairing/) subtree on every container start, regardless of the
   top-level decision. The directory is tiny (a few JSON files), so
   the unconditional chown is effectively free. Container restart
   now self-heals.

2. gateway/pairing.py: PairingStore._load_json was swallowing
   PermissionError under its bare 'except OSError' branch, which is
   what made this a silent failure. Split it out: log a WARNING that
   names the file, the gateway's uid, the file's owner/mode, and the
   exact docker exec -u hermes workaround. Still falls back to {} so
   the gateway stays up.

3. website/docs/user-guide/security.md: add a Docker tip to the
   pairing-CLI section pointing users at `docker exec -u hermes …`
   up front.

Reproduced end-to-end in a containerized harness — before the fix
the gateway sees 0 approved users after `docker exec` + restart;
after the fix it sees the expected 1, and the file on disk goes
from `root:root 600` back to `hermes:hermes 600` on next start.

Fixes #10270

* fix(pairing): gate os.geteuid for Windows in PermissionError warning
2026-07-05 14:48:50 -07:00
Teknium
8986981df4
security(gateway): set explicit client_max_size on 3 uncapped aiohttp servers (#59180)
Sibling sweep from the #58902 raft review found aiohttp servers still
running on the implicit 1 MiB default with no explicit body cap:

- bluebubbles webhook (127.0.0.1): 1 MiB explicit cap — events are small
  JSON/form payloads; attachments arrive via the REST API
- teams Bot Framework listener (0.0.0.0 bind — most exposed): 1 MiB cap;
  activities are JSON well under that
- hermes proxy server: 10 MB cap mirroring api_server's MAX_REQUEST_BYTES
  (chat-completion payloads can be large, but must stay bounded)

client_max_size bounds every read path including chunked transfer-encoding
requests that carry no Content-Length (#58536/#58902 pattern).

Deliberately excluded: feishu, whatsapp_cloud, sms, line, wecom, msgraph —
open contributor PRs (#54938, #54944, #54620, #54931, #54934, #25296)
already cover those; reviewing them separately preserves their credit.

3 regression tests pin the wiring.
2026-07-05 14:48:28 -07:00
Teknium
65117671e3
fix(gateway): apply platform-disabled skill gate to bundle invocations (#59156)
Skill bundles load their member skills via _load_skill_payload directly,
bypassing the scan-time disabled filter in get_skill_commands(). PR #58888
closed this gap for stacked slash-skill invocations, but /<bundle> dispatch
in the gateway had the same class of bypass: a skill an operator disabled
for a platform via skills.platform_disabled still got its full content
injected when referenced by a bundle.

build_bundle_invocation_message() now accepts a platform kwarg, filters
members against get_disabled_skill_names(platform=...), and reports skipped
skills in the bundle header. Gateway dispatch passes the event's platform
explicitly (env-var resolution can't be trusted in the multi-platform
gateway process, same reasoning as the #58888 gate).
2026-07-05 14:48:11 -07:00
Teknium
cd2b360d64 feat: add Docker terminal network toggle
Port from qwibitai/nanoclaw#2713: expose Hermes' existing Docker network isolation primitive through terminal config so operators can opt out of container egress.
2026-07-05 14:41:51 -07:00
Teknium
747386ecfa
refactor: consolidate gateway session metadata into state.db (#58899)
Moves gateway routing metadata (display_name, origin_json, expiry_finalized)
into state.db, making SQLite the single source of truth for gateway session
discovery. Eliminates the dual-file (sessions.json + state.db) polling
dependency that caused the mcp_serve new-conversation race (#8925).

- hermes_state.py: schema v18 (3 new sessions columns + sessions.json
  backfill migration), record_gateway_session_peer gains
  display_name/origin_json, new set_expiry_finalized(),
  list_gateway_sessions(), find_session_by_origin()
- gateway/session.py: peer recorder persists display_name + full origin
  JSON; new SessionStore.set_expiry_finalized() single write-path
- gateway/run.py: expiry watcher success + give-up paths use the store
  helper so the flag lands in both sessions.json and state.db
- mcp_serve.py: routing index reads state.db first (sessions.json fallback
  for pre-migration DBs); _poll_once collapses to a single state.db mtime
  check — the #8925 race is structurally impossible now
- gateway/mirror.py, gateway/channel_directory.py, hermes_cli/status.py:
  query state.db first, sessions.json fallback

Closes #9006
2026-07-05 14:01:03 -07:00
srojk34
cdcbc3a31d fix(gateway): clear last-resolved-model cache on 3 more conversation-boundary resets
11b4a21a5 cleared the per-session _last_resolved_model cache on /new and
the compression-exhausted auto-reset, so a resumed/reset conversation
resolves the model from current config instead of a stale cached value
(#58403). Three other sites documented as the same "full conversation
boundary" treatment — pop _session_model_overrides, clear the reasoning
override, pop _pending_model_notes — still missed _last_resolved_model:

- _session_expiry_watcher's permanent finalization block (gateway/run.py):
  a session that goes idle and is finalized, then resumed, could serve a
  model cached before it went idle on a transient config-cache miss.
- The daily/idle/suspended auto-reset cleanup (_was_auto_reset handling,
  gateway/run.py): same failure mode, different trigger.
- /resume (gateway/slash_commands.py), whose own comment already says
  "conversation boundary just like /new" for the sibling dicts it clears.

Fix: pop the session's _last_resolved_model entry in all three, mirroring
the exact pattern 11b4a21a5 established.
2026-07-05 13:58:03 -07:00
srojk34
04d732dc56 fix(gateway): re-check every stacked skill against the platform-disabled list
_handle_message() re-checks a slash-skill command's per-platform disabled
status before dispatch, because get_skill_commands() only applies the
global disabled list at scan time. That check only covered the leading
skill: split_stacked_skill_commands() resolves additional /skill tokens
that follow it (stacked invocations, up to 5 skills, #57987), and
build_stacked_skill_invocation_message() loads every one of them via
_load_skill_payload() with no disabled-status check of any kind.

A message on a platform with skills.platform_disabled configured for a
given skill could still get that skill's full SKILL.md content injected
into the agent's context for the turn, as long as it was typed after an
allowed skill: `/allowed-skill /disabled-skill do X`.

Fix: after computing the stacked extra_keys, look up each one's skill
name and re-check it against the same get_disabled_skill_names(platform=)
set already used for the leading skill. If any stacked skill is disabled
for the platform, reject the whole invocation with the same style of
message the leading-skill check already returns, instead of partially
loading it.
2026-07-05 13:57:11 -07:00
heathley
9d2ff58f5f fix(yuanbao): skip resource resolve on cache hits 2026-07-05 13:53:30 -07:00
Teknium
55e3ee1ab8
fix: remove dead f-string prefixes via ruff F541 (216 sites) (#52336)
ruff check --fix --select F541 . on current main. Pure prefix removals;
adjacent-string concatenations keep the f only on interpolating fragments.
No string content or live placeholder altered.
2026-07-05 13:42:46 -07:00
kshitijk4poor
18058c4515 fix(gateway): drain housekeeping thread over its own 30s future on shutdown
Follow-up on the #58818 cron-drain fix. The housekeeping ticker uses the
same loop-scheduled-future pattern as cron — it refreshes the channel
directory via safe_schedule_threadsafe(build_channel_directory(...), loop)
and blocks on fut.result(timeout=30). The original fix swapped its
join(5) for _await_thread_exit(5), which is a strict improvement (the loop
stays alive so the future can run) but the 5s bound is shorter than the
30s future, so a refresh in flight at shutdown was still abandoned. Bound
the housekeeping drain at 35s (30s future + margin) via a dedicated
_HOUSEKEEPING_SHUTDOWN_DRAIN_TIMEOUT constant. Not user-facing (self-heals
next tick) but keeps the cooperative drain honest across both threads.
2026-07-06 01:37:10 +05:30
HexLab98
dcd70c5823 fix(gateway): drain in-flight cron delivery on restart instead of dropping it
A cron delivery uses the live adapter by scheduling the send coroutine onto the
gateway event loop (safe_schedule_threadsafe) and blocking the ticker thread on
future.result(). On shutdown/restart the cleanup ran a synchronous
cron_thread.join(timeout=5), which blocks the event loop — so the pending
delivery coroutine could never execute, the join always timed out, and the
message was silently dropped (#58818). The default agent.restart_drain_timeout
is 0, so this fired on every restart with an in-flight delivery.

Replace the blocking joins with _await_thread_exit(), which polls is_alive()
via await asyncio.sleep so the loop keeps running and finishes the queued
delivery before teardown. The cron wait is bounded by the delivery future's own
60s ceiling (plus margin); housekeeping keeps a short bound. When no delivery is
in flight the ticker exits on stop_event immediately, so shutdown stays snappy.
2026-07-06 01:37:10 +05:30
kshitijk4poor
123c6f3a23 fix(config): close unreadable-overwrite bug class at a single chokepoint
The unreadable-config-overwrite bug (an existing config.yaml that reads as
{} on a permission/IO error gets replaced with only defaults or the edited
section) is not limited to save_config / config set / auth. The same
read-then-atomic_yaml_write pattern lives at ~7 other independent write
sites that don't route through those functions:

  - gateway/slash_commands.py: _save_config_key, memory/skills write_approval
    toggles, tool_progress toggle, runtime_footer toggle, personality set
  - hermes_cli/doctor.py --fix (stale root-key migration)
  - gateway/platforms/yuanbao.py auto-sethome
  - plugins/platforms/telegram/adapter.py topic thread_id persistence
  - tui_gateway/server.py _save_cfg
  - agent/onboarding.py mark_seen

Rather than sprinkle require_readable_config_before_write() at each site,
add a single fail-closed chokepoint, atomic_config_write(), that runs the
guard then delegates to atomic_yaml_write, and route every config.yaml
write through it. Root cause remains that read_raw_config() can't tell an
absent file from an unreadable one (returns {} for both) — read-only
callers correctly stay fail-open, but any full-file replacement now fails
closed in one enforced place instead of relying on each caller to remember
the guard.

save_config / set_config_value / auth keep the contributor's original
guard calls (their commit); this commit widens the fix to the sibling
call paths and adds a regression test on the chokepoint (fails closed on
unreadable existing file + still creates a genuinely absent file).
2026-07-05 23:00:34 +05:30
devatnull
14c91ade32 fix: normalize display boolean strings 2026-07-05 06:29:26 -07:00
devatnull
b9de7044aa fix: preserve log tool-progress mode with status phrases 2026-07-05 06:29:26 -07:00
devatnull
d111faa3a7 fix: preserve busy steer env override 2026-07-05 06:29:26 -07:00
devatnull
12f03b11ff feat: make busy steer ack configurable 2026-07-05 06:29:26 -07:00
devatnull
46fbd73f66 fix: strip tool progress display modes 2026-07-05 06:29:26 -07:00
devatnull
fddc95f4c2 chore: limit generic status phrases to long-running notifications 2026-07-05 06:29:26 -07:00
devatnull
4bf5b563bd feat: add generic gateway status phrases 2026-07-05 06:29:26 -07:00
devatnull
11627fdcb9 feat(whatsapp): native Baileys polls, clarify-as-poll, locations, and rich inbound metadata
Salvaged from PR #58704 by @devatnull, scoped to the WhatsApp surface:
- bridge_helpers.js: pure, tested extraction of inbound Baileys message
  parsing (quoted text, MIME/filename, PTT vs audio, stickers, contacts,
  reactions, polls, locations, GIF playback metadata)
- native poll primitive: /send-poll endpoint, poll messageSecret caching,
  encrypted vote decryption + aggregation via Baileys
- send_clarify() renders multi-choice clarify prompts as native polls;
  votes flow back through the existing clarify text-intercept
- send_location() + /send-location for native WhatsApp location pins
- structured quoted-reply context (fixes duplicated '[Replying to: ...]'
  rendered both by the adapter and gateway/run.py)
- outbound formatting: markdown *italic* -> WhatsApp _italic_, invisible
  unicode sanitization; execSync -> execFileSync hardening; GIF -> mp4
  gifPlayback conversion with truthful image/gif fallback

Out of scope (deliberately not salvaged from #58704): cross-platform
ordered-delivery machinery in gateway/platforms/base.py, LOCATION: and
hermes:poll response-text directives (no prompt wiring exists yet), and
the unconditional WhatsApp reply-anchor suppression.
2026-07-05 06:27:20 -07:00
devatnull
4be749d151 fix: honor top-level STT transcript echo config 2026-07-05 06:12:49 -07:00
devatnull
406eb719c3 fix: gate interrupt STT transcript echoes 2026-07-05 06:12:49 -07:00
devatnull
bfc5262725 feat: add STT transcript echo toggle 2026-07-05 06:12:49 -07:00
MorAlekss
d577408f3f fix(webhook): reject generic V2 signature missing timestamp instead of falling back to V1 2026-07-05 02:25:38 -07:00
Teknium
cb6c47af08
feat(approvals): /deny <reason> relays denial reason to the agent (port nanoclaw#2832) (#54518)
* feat(approvals): /deny <reason> relays denial reason to the agent

Port from qwibitai/nanoclaw#2832 (reject with reason).

Gateway /deny now accepts an optional trailing reason (/deny <reason>
or /deny all <reason>). The reason rides on the per-session approval
entry through resolve_gateway_approval -> _await_gateway_decision and is
appended to the BLOCKED tool result the agent receives, so a declined
agent can adapt instead of only hearing 'denied'.

Adapted to hermes-agent's synchronous single-command /deny model: no DB
state, no second-message capture step, no migration. Reason is capped at
280 chars and threaded through both the terminal-command guard and the
execute_code guard. Plain /deny and the approve paths are unchanged.

- tools/approval.py: _ApprovalEntry.reason; resolve_gateway_approval gains
  optional reason; _await_gateway_decision returns it; both gateway BLOCKED
  messages include it
- gateway/slash_commands.py: parse leading 'all' + trailing reason
- locales/en.yaml: deny.denied_reason_{singular,plural}
- hermes_cli/commands.py: /deny args_hint '[all] [reason]'
- tests: 3 new (with-reason, all+reason, plain-deny regression)

* fix(ci): localize deny-reason keys across all locales + update interrupt-path assertions

CI surfaced two enforced invariants broken by the deny-with-reason change:
- test_i18n catalog-parity requires every locale to carry the same keys as
  en.yaml with matching placeholders. Added deny.denied_reason_singular/plural
  (with {count}/{reason}) to all 15 non-English locales.
- test_approval_interrupt asserts the exact dict from _await_gateway_decision,
  which now carries a 'reason' key (None on the interrupt/timeout paths).
2026-07-05 02:22:08 -07:00
Teknium
9767e19b60
feat(skills): stacked slash-skill invocations — /skill-a /skill-b do XYZ (#57987)
Inspired by Claude Code v2.1.199 (July 2, 2026): stacked slash-skill
invocations load all leading skills (up to 5), not just the first.

- agent/skill_commands.py: split_stacked_skill_commands() consumes leading
  /skill tokens (stops at the first non-skill token so slash-path arguments
  are never swallowed); build_stacked_skill_invocation_message() composes
  the multi-skill turn reusing the existing bundle scaffolding markers so
  extract_user_instruction_from_skill_message() keeps memory providers
  storing the user's instruction, not N skill bodies.
- cli.py + gateway/run.py: dispatch the stacked path on both surfaces.
- 11 new tests + docs section in skills.md.
2026-07-05 02:20:01 -07:00
luyifan
30479961b8 fix(gateway): tolerate punctuation on silence markers 2026-07-05 02:12:26 -07:00
teknium1
708b57e009 fix(webhook): rate-limit V1 deprecation warning + document V2 signature
- warn once per route instead of on every request (busy senders would
  spam the log)
- document X-Webhook-Signature-V2 / X-Webhook-Timestamp in the webhooks
  user guide

Follow-ups for salvaged #58461.
2026-07-05 01:36:53 -07:00
MorAlekss
70449a4939 fix(security): add timestamp-bound V2 signature for generic webhook replay protection 2026-07-05 01:36:53 -07:00
Teknium
f23026f979
Merge pull request #58536 from NousResearch/salvage/3955-webhook-chunked-limit
fix(gateway): enforce body-size limits on chunked requests (salvage #3955 + #3949)
2026-07-05 00:44:37 -07:00
Teknium
485ae54c9f
fix(gateway): pass full transcript to compressor instead of filtered messages (#58551)
Both gateway compression entry points (session-hygiene auto-compress in
run.py; manual /compress in slash_commands.py) filtered the transcript
to user/assistant-only, content-bearing messages before calling
_compress_context. That starved the compressor:

- tool results are usually the bulk of the context, and
  _prune_old_tool_results never saw them
- short filtered histories tripped the protect-first/last early-return,
  so compression became a no-op even on huge sessions
- assistant tool_calls stubs (content=None) were dropped, so even the
  summary lost the tool activity

Pass user/assistant/tool messages through intact, matching what the
agent loop itself feeds _compress_context.

Port of PR #3854 onto current main (the manual-compress handler moved
from run.py to slash_commands.py since the PR branched); regression test
asserts tool messages reach the compressor.

Authored-by: David Zhang <david.d.zhang@gmail.com> (@Git-on-my-level)

Co-authored-by: David Zhang <david.d.zhang@gmail.com>
2026-07-05 00:43:48 -07:00
Sahil Shubham
6c7960cfa0 fix(whatsapp_cloud): honor documented WHATSAPP_CLOUD_ALLOWED_USERS / ALLOW_ALL_USERS
The Cloud setup wizard and docs tell operators to set
WHATSAPP_CLOUD_ALLOWED_USERS (and WHATSAPP_CLOUD_ALLOW_ALL_USERS), but the
adapter DM intake gate only read WHATSAPP_CLOUD_ALLOW_FROM + WHATSAPP_CLOUD_DM_POLICY
(default open, opted-in only via GATEWAY_/WHATSAPP_ALLOW_ALL_USERS). So an
allowlist set via the documented var silently dropped every inbound
(_should_process_message -> None -> HTTP 200, no dispatch, no log line).

- _allow_from also reads WHATSAPP_CLOUD_ALLOWED_USERS
- dm_policy defaults to allowlist when an allowlist is present (else open)
- _open_dm_opted_in() also honors WHATSAPP_CLOUD_ALLOW_ALL_USERS

Explicit DM_POLICY / ALLOW_FROM still win -> backward compatible.
2026-07-05 00:41:34 -07:00
liuhao1024
132bb8a163 fix(yuanbao): restore active singleton after WS reconnect
_do_reconnect() succeeded but never called
YuanbaoAdapter.set_active(adapter), leaving get_active()
permanently returning None after any WS disconnect/reconnect
cycle. This caused cron delivery to silently fail because
_send_yuanbao() checks get_active_adapter() and gives up
immediately when it returns None.

Fix: call set_active(adapter) after successful reconnect,
matching the pattern in connect().

Fixes #58363
2026-07-05 00:41:34 -07:00
luyifan
5b8593266f fix(gateway): cap proxy SSE line buffer 2026-07-05 00:41:34 -07:00
kshitijk4poor
1388cd1c0c fix(logging): thread-safe queue state + bounded hard-exit drain + record copy
Self-review (3-agent + codex) findings on the async QueueListener change:

1. (HIGH) The os._exit shutdown backstop called flush_log_queue(), whose
   stop() joins the listener thread unbounded. If that thread is wedged on
   the rotation lock — the exact failure this change survives — shutdown
   re-freezes. Add drain_log_queue(timeout): stop-only, bounded via a
   throwaway joiner thread. Also release PID/runtime locks BEFORE the drain
   so a slow drain can't strand them.

2. (MED) _log_queue/_queue_listener/_queued_file_handlers were read-modify-
   written without a lock across register/stop/flush/reset; a gateway-init
   race with a plugin/CLI path could leave two live listeners. Guard all
   four globals with a single _queue_state_lock.

3. (MED) _NonFormattingQueueHandler.prepare() enqueued the same LogRecord a
   synchronous handler on the emitting thread may still format/mutate.
   Return copy.copy(record) (preserves msg/args/exc_info for deferred
   RedactingFormatter) to remove the cross-thread mutation race.

E2E-verified: bounded drain returns in ~500ms on a permanently-wedged
listener; 4x20 concurrent flushes single-listener no-crash; args still
format and secrets still redact through the copied record.
2026-07-05 11:44:53 +05:30
kshitijk4poor
ac68a6411a fix(gateway): drain async log queue on os._exit shutdown backstop
The QueueListener change routes rotating file handlers through an
in-memory queue drained on a dedicated thread, with an atexit hook to
flush on shutdown. But _exit_after_graceful_shutdown() uses os._exit,
which bypasses atexit — so on the early-exit and #53107 hard-exit paths
the queued records (including the shutdown reason) were silently lost.

Explicitly flush_log_queue() before os._exit, and correct the now-stale
comment that claimed handlers are synchronous with nothing pending.
2026-07-05 11:44:53 +05:30
yoma
7e8f50a141 fix(gateway): load display config from routed profile 2026-07-04 16:34:16 -07:00
liuhao1024
11b4a21a56 fix(gateway): clear last-resolved-model cache on /new and compression auto-reset
After a config change (e.g. switching model provider), the /new command
must clear the per-session _last_resolved_model cache so the next turn
resolves the model from the updated config instead of falling back to
the stale cached value.

Without this fix, if a transient config-cache miss occurs on the first
post-/new turn, the #35314 recovery path serves the old model from the
cache — the user sees the old model being used even though they changed
config.yaml and explicitly ran /new.

Fix applies to both call sites that reset session model state:
- GatewaySlashCommandsMixin._handle_reset_command (slash_commands.py)
- GatewayRunner compression-exhausted auto-reset (run.py)

Fixes #58403
2026-07-04 16:34:16 -07:00
Tuna Dev
ff4c8172ce fix(gateway): attach credential_pool to session /model overrides
Per-session /model overrides supplied api_key and provider but omitted
credential_pool, so billing rotation never ran on HTTP 402. Wire the pool
on fast override, rehydrate, and apply paths; backfill from provider for
legacy persisted overrides. Regression tests in tests/gateway/.
2026-07-04 16:34:16 -07:00
Gutslabs
2b4ec0082a
fix(api_server): return 413 for oversized chunked bodies
api_server already caps every read via client_max_size (chunked
included), but when the limit tripped mid-read the handler's broad JSON
except turned it into 400 'Invalid JSON'. Catch
HTTPRequestEntityTooLarge in body_limit_middleware and return the
OpenAI-style 413.

Status-code polish extracted from PR #3949 by @Gutslabs — the PR's core
client_max_size change already exists on main.
2026-07-04 15:35:05 -07:00
Gutslabs
ec29590a0f
fix(webhook): enforce body-size limit on chunked requests
The webhook adapter enforced max_body_bytes only via the Content-Length
header; a Transfer-Encoding: chunked request (content_length=None) or a
spoofed small Content-Length bypassed the cap entirely and read the full
body (bounded only by aiohttp's implicit 1 MiB default, above any
operator-configured smaller limit).

- web.Application(client_max_size=max_body_bytes): aiohttp enforces the
  cap on every read path, chunked included
- catch HTTPRequestEntityTooLarge -> 413 (was swallowed into generic 400)
- post-read length re-check as defense in depth
- chunked-upload regression test

Manual port of PR #3955 by @Gutslabs onto current main (handler had
been restructured since); authorship preserved.
2026-07-04 15:33:31 -07:00
teknium1
c6dc7c03c3
Revert "Merge pull request #30179 from NousResearch/feat/iron-proxy"
This reverts commit 8790adc4c6, reversing
changes made to fe5054bccf.
2026-07-04 13:38:59 -07:00
Teknium
8790adc4c6
Merge pull request #30179 from NousResearch/feat/iron-proxy
feat(egress): iron-proxy credential-injection firewall for sandboxes
2026-07-04 13:29:23 -07:00
infinitycrew39
1cc68fc897 fix(gateway): resolve terminal.cwd placeholders per backend and mount mode
Split placeholder TERMINAL_CWD resolution into three cases: local falls
back to MESSAGING_CWD/home; docker without workspace mount leaves cwd unset;
docker with mount enabled preserves an explicit host MESSAGING_CWD path for
terminal_tool's /workspace mapping. Stops leaking host Path.home() into
containers without breaking the mount contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 13:28:47 -07:00
teknium1
14cbbd541e
Merge remote-tracking branch 'origin/main' into iron-proxy-followups
# Conflicts:
#	hermes_cli/config.py
#	hermes_cli/main.py
#	website/docs/reference/cli-commands.md
2026-07-04 03:09:43 -07:00
kshitijk4poor
4651ac64a1 refactor(ssh): extract shared _is_ssh_remote_tilde_cwd predicate
Follow-up to the salvaged SSH-tilde-cwd fix. The predicate
"backend == ssh and (cwd == ~ or cwd.startswith(~/))" was inlined at
each expanduser guard site, which is how the test simulator drifted from
production (it grew an SSH guard on a top-level-alias branch that has no
production counterpart).

- Add tools/terminal_tool._is_ssh_remote_tilde_cwd(backend, cwd) as the
  single source of truth (case/whitespace-tolerant).
- Use it in _get_env_config and the gateway config bridge.
- Test simulator imports the real helper instead of re-implementing the
  predicate; revert the phantom SSH guard on the top-level-alias branch
  (production maps top-level cwd: to a plain env var, not TERMINAL_CWD via
  an SSH-guarded path — that branch tested nothing real).
2026-07-04 14:17:11 +05:30
helix4u
83fb8ec277 fix(ssh): preserve remote tilde cwd 2026-07-04 14:17:11 +05:30