Skip to content

fix(hermes): add Discord facade#3153

Open
ericksoa wants to merge 8 commits intomainfrom
fix/hermes-discord-proxy-bridge
Open

fix(hermes): add Discord facade#3153
ericksoa wants to merge 8 commits intomainfrom
fix/hermes-discord-proxy-bridge

Conversation

@ericksoa
Copy link
Copy Markdown
Contributor

@ericksoa ericksoa commented May 7, 2026

Summary

  • add a Hermes-only Discord facade inside the sandbox so discord.py sees normal Discord REST and Gateway behavior without exposing the real bot token to sandbox-owned config, env, argv, logs, or local WebSocket frames
  • redirect Hermes' discord.py Discord transports to the loopback facade with a Python sitecustomize preload while keeping Hermes provider semantics unchanged (commands.Bot(...) and client.start(self.config.token) still run normally)
  • keep real Discord REST auth on OpenShell's existing placeholder-substitution path by forwarding outbound REST through the local decode proxy with Authorization: Bot openshell:resolve:env:DISCORD_BOT_TOKEN
  • add signed HTTP interaction ingress and focused tests proving local Gateway startup, placeholder-only IDENTIFY, REST forwarding, interaction token localization, preload rewrites, and synthesized Gateway dispatches

How the fix works

Hermes still writes and consumes the Discord token as openshell:resolve:env:DISCORD_BOT_TOKEN. At runtime, start.sh now starts two local helpers before the Hermes gateway: the existing URL decode proxy on 127.0.0.1:3129, then the new Discord facade on 127.0.0.1:3130. Hermes is launched with DISCORD_PROXY=http://127.0.0.1:3129, NEMOCLAW_DISCORD_FACADE_URL=http://127.0.0.1:3130, and PYTHONPATH=/opt/nemoclaw-hermes-discord-preload....

The preload patches aiohttp calls made by discord.py. Requests to discord.com / discordapp.com API paths are rewritten to the loopback facade. Gateway WebSocket connections to gateway.discord.gg are rewritten to ws://127.0.0.1:3130/gateway. This means discord.py still emits its normal REST requests and Gateway IDENTIFY / RESUME frames, but they terminate at a sandbox-local Discord-compatible service instead of crossing the OpenShell WebSocket tunnel with an unresolved placeholder token.

The facade accepts the placeholder token only. It sends the expected Gateway HELLO, heartbeat ACKs, READY, and RESUMED dispatches, tracks sequence/session state, and rejects non-placeholder local Gateway tokens with close code 4004. Startup REST endpoints that discord.py needs are emulated locally, and application-command endpoints are stored in memory so slash command registration can complete without leaking secrets.

REST calls the facade does not emulate are forwarded to real Discord through the decode proxy. The forwarded Authorization header remains Bot openshell:resolve:env:DISCORD_BOT_TOKEN, so OpenShell remains the only layer that resolves the real credential. The facade process itself is launched with the preload disabled so it cannot accidentally rewrite its own upstream Discord traffic back into localhost.

Discord interactions are received through the HTTP outgoing-webhook path. The facade validates Discord Ed25519 signatures on /interactions, handles PINGs directly, maps real interaction tokens to memory-only nemoclaw-local-* tokens, and injects non-PING payloads into the local Gateway stream as INTERACTION_CREATE. Followup/callback routes translate the local token back to the real interaction token only inside the facade's memory before forwarding through the REST proxy path.

For non-interaction Gateway event coverage, the facade includes a REST poller that can synthesize message, edit, delete, reaction, thread, channel, guild, and member-style deltas into normal Gateway dispatches. Reaction polling uses a stable synthetic user id because Discord REST reaction aggregates do not expose the individual reacting user.

Security boundary

  • The sandbox-local Gateway accepts only openshell:resolve:env:DISCORD_BOT_TOKEN or other openshell:resolve:env:* placeholders, never the real bot token.
  • Real bot-token resolution remains in OpenShell's outbound REST credential resolver.
  • Real Discord interaction tokens are not placed on local WebSocket payloads; they are replaced with memory-only local tokens before dispatch.
  • The facade redacts interaction-token paths in logs and keeps interaction-token mappings in process memory only.
  • No host listener or host bridge is required for the facade. Optional public interaction ingress is via a sandbox-started outbound HTTPS tunnel when configured.

Runtime wiring changed

  • agents/hermes/discord-facade.py implements the local REST/Gateway facade and interaction ingress.
  • agents/hermes/discord-preload/sitecustomize.py rewrites discord.py Discord REST/Gateway transports to the facade.
  • agents/hermes/start.sh starts the facade and exports the preload/facade environment for Hermes.
  • src/lib/agent-runtime.ts includes the facade in Hermes manual recovery startup.
  • agents/hermes/config/messaging-config.ts writes the facade URL and Discord guild ids into Hermes env.
  • agents/hermes/policy-additions.yaml allows the Discord REST methods/paths needed for command, message, reaction, and webhook operations.

Validation

  • python3 -m py_compile agents/hermes/discord-facade.py agents/hermes/decode-proxy.py
  • bash -n agents/hermes/start.sh test/e2e/test-hermes-discord-e2e.sh
  • npm run build:cli
  • npx vitest run test/hermes-discord-facade.test.ts test/generate-hermes-config.test.ts src/lib/agent-runtime.test.ts test/sandbox-init.test.ts test/policies.test.ts
  • git diff --check
  • commit/pre-push hooks, including secret scanning, shellcheck, hadolint, TypeScript CLI build, package/tag sync, CLI tests, and source-shape budget

Not run

  • Full live Hermes Discord E2E. That still requires the live sandbox/OpenShell/Docker credential path and a configured Discord app/interactions endpoint.

Summary by CodeRabbit

  • New Features

    • Added Discord channel integration with integrated proxy facade for REST and WebSocket gateway traffic
    • Implemented transparent URL rewriting for Discord REST API and gateway connections
    • Added Discord-specific network security policies
  • Tests

    • Expanded end-to-end testing coverage for Discord functionality
    • Added comprehensive unit tests for Discord facade behavior and URL rewriting

@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented May 7, 2026

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a local Discord facade server that intercepts and bridges Discord REST and Gateway traffic for Hermes. It includes the facade implementation in Python, network interception via aiohttp preload monkey-patching, sandbox policies, Docker build configuration, startup orchestration, recovery logic, and comprehensive E2E and unit tests.

Changes

Discord Facade & Network Bridging

Layer / File(s) Summary
Policy & Configuration
agents/hermes/config/messaging-config.ts, agents/hermes/policy-additions.yaml, agents/hermes/policy-permissive.yaml
Module constants define local Discord proxy/facade URLs. DISCORD_PROXY and NEMOCLAW_DISCORD_FACADE_URL environment variables are emitted when Discord is enabled. Policy files allow REST operations (PUT/PATCH/DELETE) on Discord API endpoints and add tls: skip to gateway endpoints.
Core Facade Server
agents/hermes/discord-facade.py
Implements aiohttp web server with /gateway WebSocket endpoint, REST route handling, gateway IDENTIFY authentication using placeholder tokens, synthetic READY emission, message/thread polling with state diffing, application command CRUD, interaction signature verification (Ed25519 via nacl or cryptography), and REST forwarding to Discord API.
Network Interception
agents/hermes/discord-preload/sitecustomize.py, agents/hermes/decode-proxy.py
Sitecustomize.py monkey-patches aiohttp to rewrite Discord REST and WebSocket URLs to local facade when NEMOCLAW_DISCORD_FACADE_URL is set. Strips proxy arguments during rewrite. Decode-proxy docstring clarified to state it relays WebSocket bytes unchanged without payload inspection.
System Integration
agents/hermes/Dockerfile, agents/hermes/start.sh
Docker build groups COPY/RUN for decode-proxy and discord-facade with combined chmod. start.sh adds start_discord_facade() function to launch facade with decode-proxy routing, waits for listening port, and integrates facade PID into cleanup. Exports DISCORD_PROXY and NEMOCLAW_DISCORD_FACADE_URL in both root and non-root execution paths.
Recovery & Runtime
src/lib/agent/runtime.ts, src/lib/agent/runtime.test.ts
hermesGatewayEnvPrefix() includes NEMOCLAW_DISCORD_FACADE_URL. Recovery command launches facade, checks both decode-proxy (3129) and facade (3130) listening ports. Tests assert environment variables, service binaries, and log preparation.
Facade Unit Tests
test/hermes-discord-facade.test.ts
Node test harness runs Python facade code with mocked aiohttp. Covers gateway IDENTIFY validation, REST forwarding, message/thread polling synthesis, interaction signature verification, and preload URL rewriting behavior.
E2E & Policy Tests
test/e2e/test-hermes-discord-e2e.sh, test/policies.test.ts, test/sandbox-init.test.ts, test/e2e/test-messaging-providers.sh, test/generate-hermes-config.test.ts
E2E test adds facade protocol probe, Phase 5-7 validation for token isolation and gateway auth boundary. Policy tests assert L4 WebSocket tunnel constraints and REST mutation scoping. Sandbox init tests verify facade startup paths. Messaging provider tests clarify transport-only behavior without payload rewriting.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A facade rises, Discord threads now flow,

Through local gates that intercept the glow,

Tokens masked, events synthesized with care,

WebSockets dance through the aiohttp snare,

Hermes speaks Discord—a bridge now complete! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: adding a Discord facade to Hermes. It is concise, specific, and directly reflects the main objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hermes-discord-proxy-bridge

Comment @coderabbitai help to get the list of available commands and usage tips.

@ericksoa ericksoa mentioned this pull request May 7, 2026
Closed
@ericksoa ericksoa marked this pull request as ready for review May 7, 2026 00:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/e2e/test-hermes-discord-e2e.sh (1)

398-403: ⚡ Quick win

Avoid keying this E2E on an exact Hermes log line.

This assertion couples CI to upstream log wording rather than behavior. Since the Hermes base image is still tracked via :latest, a harmless message-text change would fail the test even if proxy routing still works. Prefer a looser match on stable substrings or rely on the existing .env/sandbox-env + reachability checks as the contract here.

Suggested tweak
-gateway_proxy_log=$(sandbox_exec "grep -F 'Using proxy for Discord: http://127.0.0.1:3129' /tmp/gateway.log 2>/dev/null | tail -1 || true")
+gateway_proxy_log=$(sandbox_exec "grep -E 'Using proxy for Discord: .*127\.0\.0\.1:3129' /tmp/gateway.log 2>/dev/null | tail -1 || true")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/test-hermes-discord-e2e.sh` around lines 398 - 403, The test is too
brittle because it greps an exact Hermes log line including the URL; update the
assertion around gateway_proxy_log (the sandbox_exec grep invocation) to match a
looser, stable substring or alternative behavior: either grep for both "proxy"
and "Discord" (without the exact host:port) or assert the DISCORD_PROXY env is
present in the sandbox-env and that outbound requests to Discord are routed
through the proxy (reachability check), then call pass/fail as before. Locate
the sandbox_exec call that assigns gateway_proxy_log and replace the exact URL
match with the looser substring or the equivalent env+reachability checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/e2e/test-hermes-discord-e2e.sh`:
- Around line 501-508: The script currently allows skipping gateway auth even
when a real Discord token is supplied; update the conditional around
gateway_auth_log to treat a provided real token as requiring gateway
authentication: after computing gateway_auth_log, if it's non-empty retain pass
("Hermes Discord gateway authenticated..."); otherwise, if the environment
variable DISCORD_BOT_TOKEN_REAL is set/non-empty, call fail (same message as the
HERMES_DISCORD_REQUIRE_GATEWAY_AUTH branch) to enforce that a real token must
produce gateway auth; keep the existing HERMES_DISCORD_REQUIRE_GATEWAY_AUTH
check for explicit test-run forcing, and only fall back to skip when neither
DISCORD_BOT_TOKEN_REAL nor HERMES_DISCORD_REQUIRE_GATEWAY_AUTH indicate a hard
requirement. Use the same identifiers gateway_auth_log, DISCORD_BOT_TOKEN_REAL,
and HERMES_DISCORD_REQUIRE_GATEWAY_AUTH to locate and modify the logic.

---

Nitpick comments:
In `@test/e2e/test-hermes-discord-e2e.sh`:
- Around line 398-403: The test is too brittle because it greps an exact Hermes
log line including the URL; update the assertion around gateway_proxy_log (the
sandbox_exec grep invocation) to match a looser, stable substring or alternative
behavior: either grep for both "proxy" and "Discord" (without the exact
host:port) or assert the DISCORD_PROXY env is present in the sandbox-env and
that outbound requests to Discord are routed through the proxy (reachability
check), then call pass/fail as before. Locate the sandbox_exec call that assigns
gateway_proxy_log and replace the exact URL match with the looser substring or
the equivalent env+reachability checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 43a72dd9-bbb6-4e16-ac19-41f310a626dc

📥 Commits

Reviewing files that changed from the base of the PR and between 09b66c6 and 170e519.

📒 Files selected for processing (13)
  • agents/hermes/Dockerfile
  • agents/hermes/config/messaging-config.ts
  • agents/hermes/decode-proxy.py
  • agents/hermes/policy-additions.yaml
  • agents/hermes/policy-permissive.yaml
  • agents/hermes/start.sh
  • src/lib/agent-runtime.test.ts
  • src/lib/agent-runtime.ts
  • test/e2e/test-hermes-discord-e2e.sh
  • test/e2e/test-messaging-providers.sh
  • test/generate-hermes-config.test.ts
  • test/policies.test.ts
  • test/sandbox-init.test.ts

Comment thread test/e2e/test-hermes-discord-e2e.sh Outdated
@ericksoa ericksoa changed the title fix(hermes): route discord gateway through proxy fix(hermes): route Discord REST through proxy May 7, 2026
@ericksoa ericksoa added v0.0.37 Release target Integration: Hermes Integration: Discord Use this label to identify Discord bot integration issues with NemoClaw. labels May 7, 2026
@ericksoa ericksoa self-assigned this May 7, 2026
@ericksoa ericksoa added the bug Something isn't working label May 7, 2026
@ericksoa ericksoa changed the title fix(hermes): route Discord REST through proxy fix(hermes): add Discord facade May 7, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agents/hermes/policy-additions.yaml`:
- Around line 93-94: The policy currently grants PUT and PATCH on the glob "/**"
(the allow entries for method: PUT and method: PATCH), which overbroadly permits
mutation across the site; update those two allow rules to the same Discord REST
scopes used by the existing DELETE rules (i.e., restrict paths to the Discord
endpoints introduced in this file such as the channel/message API paths used
elsewhere), replacing the "/**" path with those specific Discord route patterns
so PUT/PATCH are limited to the Discord surface only.

In `@agents/hermes/start.sh`:
- Around line 283-309: The start_discord_facade function currently redirects
stdout/stderr to /tmp/discord-facade.log from the parent shell
(">/tmp/discord-facade.log"), which allows a TOCTOU/symlink attack; fix it by
using the project’s hardened/no-follow log helper to open the log file safely
and perform the redirection inside the child process after privilege drop (or
pass a safe file descriptor/path returned by the helper into the child),
ensuring the parent shell does not create or follow the /tmp symlink; update
both branches that call gosu/python3 in start_discord_facade to reuse that safe
log setup instead of direct redirection to /tmp/discord-facade.log.

In `@src/lib/agent-runtime.ts`:
- Around line 160-161: The readiness wait loops in
hermesDecodeProxyRecoveryCommand treat a missing ss binary as a success because
the break condition uses '! command -v ss ... || ss -tln ... | grep -q ... &&
break'; change both wait-loop break predicates to require ss to exist AND the
port to be found, i.e. use 'command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null
| grep -q "127.0.0.1:3129" && break' (and similarly for 3130) so the loops only
break when ss is present and the service port is actually listening.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 30a96de5-51dc-4bfc-90b9-2c528b72caf2

📥 Commits

Reviewing files that changed from the base of the PR and between 170e519 and f4ee573.

📒 Files selected for processing (12)
  • agents/hermes/Dockerfile
  • agents/hermes/config/messaging-config.ts
  • agents/hermes/discord-facade.py
  • agents/hermes/discord-preload/sitecustomize.py
  • agents/hermes/policy-additions.yaml
  • agents/hermes/start.sh
  • src/lib/agent-runtime.test.ts
  • src/lib/agent-runtime.ts
  • test/e2e/test-hermes-discord-e2e.sh
  • test/generate-hermes-config.test.ts
  • test/hermes-discord-facade.test.ts
  • test/sandbox-init.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/agent-runtime.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/generate-hermes-config.test.ts
  • agents/hermes/config/messaging-config.ts
  • agents/hermes/Dockerfile

Comment thread agents/hermes/policy-additions.yaml Outdated
Comment thread agents/hermes/start.sh
Comment thread src/lib/agent-runtime.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/e2e/test-hermes-discord-e2e.sh`:
- Around line 597-604: The one-shot grep for "Connected as " in /tmp/gateway.log
(variable gateway_auth_log) can race with the gateway IDENTIFY→READY handshake;
replace it with a retry/wait loop that calls sandbox_exec "grep -E 'Connected as
' /tmp/gateway.log" repeatedly (e.g., sleep 1 between attempts) up to a short
timeout (e.g., 30s) and sets gateway_auth_log when a match is found; after the
loop keep the existing pass/fail logic that uses gateway_auth_log, ensuring you
reuse the same sandbox_exec, gateway_auth_log variable, and pass/fail calls so
behavior is unchanged except for waiting for the READY line.
- Around line 409-414: The single-shot facade health probe using sandbox_exec
and curl (checking facade_health for '"ok":true') can race with the facade
startup; change the check in the test-hermes-discord-e2e.sh script to retry: run
sandbox_exec "curl -sf http://127.0.0.1:3130/health ..." in a loop (e.g., up to
15 attempts with sleep 4 between tries), update facade_health on each attempt,
break on success, then call pass "Hermes fake Discord facade is healthy..." if
succeeded or fail "Hermes fake Discord facade did not answer health probe:
${facade_health:0:200}" if all retries exhausted; keep use of sandbox_exec,
pass, and fail symbols intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1c71782f-95ee-406c-9285-c7648029d157

📥 Commits

Reviewing files that changed from the base of the PR and between f4ee573 and 3da5c73.

📒 Files selected for processing (1)
  • test/e2e/test-hermes-discord-e2e.sh

Comment thread test/e2e/test-hermes-discord-e2e.sh Outdated
Comment thread test/e2e/test-hermes-discord-e2e.sh
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/agent-runtime.ts`:
- Around line 160-161: The hermesDecodeProxyRecoveryCommand string currently
redirects the discord facade output to a parent-shell path
(>/tmp/discord-facade.log) which is unsafe; update
hermesDecodeProxyRecoveryCommand so it does not perform shell-level redirection
in the parent but instead invokes the child process with the same no-follow log
setup used in agents/hermes/start.sh (i.e., remove the parent-side
">/tmp/discord-facade.log 2>&1" and ensure the python3
/usr/local/bin/nemoclaw-discord-facade child does its own safe logfile open with
O_NOFOLLOW/O_EXCL or the existing helper used by start.sh), preserving the same
environment and background/port-check logic around
hermesDecodeProxyRecoveryCommand.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6c8f08bf-451c-45fd-9046-94158e05a4f5

📥 Commits

Reviewing files that changed from the base of the PR and between 3da5c73 and a797ab9.

📒 Files selected for processing (7)
  • agents/hermes/policy-additions.yaml
  • agents/hermes/start.sh
  • src/lib/agent-runtime.test.ts
  • src/lib/agent-runtime.ts
  • test/e2e/test-hermes-discord-e2e.sh
  • test/policies.test.ts
  • test/sandbox-init.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/agent-runtime.test.ts
  • test/e2e/test-hermes-discord-e2e.sh

Comment thread src/lib/agent-runtime.ts Outdated
Comment on lines +160 to +161
function hermesDecodeProxyRecoveryCommand(): string {
return 'if ! command -v ss >/dev/null 2>&1 || ! ss -tln 2>/dev/null | grep -q "127.0.0.1:3129"; then nohup python3 /usr/local/bin/nemoclaw-decode-proxy >/dev/null 2>&1 & for _i in 1 2 3 4 5 6 7 8 9 10; do ! command -v ss >/dev/null 2>&1 || ss -tln 2>/dev/null | grep -q "127.0.0.1:3129" && break; sleep 0.5; done; fi;';
return 'if ! command -v ss >/dev/null 2>&1 || ! ss -tln 2>/dev/null | grep -q "127.0.0.1:3129"; then nohup python3 /usr/local/bin/nemoclaw-decode-proxy >/dev/null 2>&1 & for _i in 1 2 3 4 5 6 7 8 9 10; do command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q "127.0.0.1:3129" && break; sleep 0.5; done; fi; if ! command -v ss >/dev/null 2>&1 || ! ss -tln 2>/dev/null | grep -q "127.0.0.1:3130"; then env -u NEMOCLAW_DISCORD_FACADE_URL -u PYTHONPATH DISCORD_PROXY=http://127.0.0.1:3129 HTTPS_PROXY=http://127.0.0.1:3129 HTTP_PROXY=http://127.0.0.1:3129 NEMOCLAW_DISCORD_FACADE_PORT=3130 nohup python3 /usr/local/bin/nemoclaw-discord-facade >/tmp/discord-facade.log 2>&1 & for _i in 1 2 3 4 5 6 7 8 9 10; do command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q "127.0.0.1:3130" && break; sleep 0.5; done; fi;';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Harden the recovery facade log the same way as startup.

The recovery command still opens >/tmp/discord-facade.log in the parent shell. If this path is reachable from a root recovery run, a sandbox-created symlink in /tmp can clobber an arbitrary file. Reuse the existing no-follow log setup here and move the redirection into the child process, mirroring agents/hermes/start.sh.

Suggested fix
 function hermesDecodeProxyRecoveryCommand(): string {
-  return 'if ! command -v ss >/dev/null 2>&1 || ! ss -tln 2>/dev/null | grep -q "127.0.0.1:3129"; then nohup python3 /usr/local/bin/nemoclaw-decode-proxy >/dev/null 2>&1 & for _i in 1 2 3 4 5 6 7 8 9 10; do command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q "127.0.0.1:3129" && break; sleep 0.5; done; fi; if ! command -v ss >/dev/null 2>&1 || ! ss -tln 2>/dev/null | grep -q "127.0.0.1:3130"; then env -u NEMOCLAW_DISCORD_FACADE_URL -u PYTHONPATH DISCORD_PROXY=http://127.0.0.1:3129 HTTPS_PROXY=http://127.0.0.1:3129 HTTP_PROXY=http://127.0.0.1:3129 NEMOCLAW_DISCORD_FACADE_PORT=3130 nohup python3 /usr/local/bin/nemoclaw-discord-facade >/tmp/discord-facade.log 2>&1 & for _i in 1 2 3 4 5 6 7 8 9 10; do command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q "127.0.0.1:3130" && break; sleep 0.5; done; fi;';
+  const facadeLogSetup =
+    `${buildNoFollowLogSetupCommand("/tmp/discord-facade.log", "gateway", "0o600")} || exit 1;`;
+  return `${facadeLogSetup} if ! command -v ss >/dev/null 2>&1 || ! ss -tln 2>/dev/null | grep -q "127.0.0.1:3129"; then nohup python3 /usr/local/bin/nemoclaw-decode-proxy >/dev/null 2>&1 & for _i in 1 2 3 4 5 6 7 8 9 10; do command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q "127.0.0.1:3129" && break; sleep 0.5; done; fi; if ! command -v ss >/dev/null 2>&1 || ! ss -tln 2>/dev/null | grep -q "127.0.0.1:3130"; then env -u NEMOCLAW_DISCORD_FACADE_URL -u PYTHONPATH DISCORD_PROXY=http://127.0.0.1:3129 HTTPS_PROXY=http://127.0.0.1:3129 HTTP_PROXY=http://127.0.0.1:3129 NEMOCLAW_DISCORD_FACADE_PORT=3130 nohup sh -c 'exec "$@" >/tmp/discord-facade.log 2>&1' sh python3 /usr/local/bin/nemoclaw-discord-facade & for _i in 1 2 3 4 5 6 7 8 9 10; do command -v ss >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q "127.0.0.1:3130" && break; sleep 0.5; done; fi;`;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/agent-runtime.ts` around lines 160 - 161, The
hermesDecodeProxyRecoveryCommand string currently redirects the discord facade
output to a parent-shell path (>/tmp/discord-facade.log) which is unsafe; update
hermesDecodeProxyRecoveryCommand so it does not perform shell-level redirection
in the parent but instead invokes the child process with the same no-follow log
setup used in agents/hermes/start.sh (i.e., remove the parent-side
">/tmp/discord-facade.log 2>&1" and ensure the python3
/usr/local/bin/nemoclaw-discord-facade child does its own safe logfile open with
O_NOFOLLOW/O_EXCL or the existing helper used by start.sh), preserving the same
environment and background/port-check logic around
hermesDecodeProxyRecoveryCommand.

@cv cv added v0.0.38 Release target and removed v0.0.37 Release target labels May 8, 2026
ericksoa added 2 commits May 7, 2026 21:03
…oxy-bridge

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/agent/runtime.ts`:
- Line 164: The ss port checks in the returned shell command string match
substrings (e.g. "127.0.0.1:3129" also matches "127.0.0.1:31290"); update both
grep invocations inside the returned string in src/lib/agent/runtime.ts so they
match the port exactly by using a stricter regex, e.g. replace grep -q
"127.0.0.1:3129" and grep -q "127.0.0.1:3130" with grep -qE
"127\\.0\\.0\\.1:3129($|\\s)" and grep -qE "127\\.0\\.0\\.1:3130($|\\s)" (or
equivalent word-boundary pattern) so the code paths around ${facadeLogSetup} and
${facadeLaunch} and the nohup python3 proxy launch use exact-port checks.
- Around line 161-164: The facade log setup currently hardcodes
/tmp/discord-facade.log (facadeLogSetup and facadeLaunch) so if that file is
unwritable the redirection fails; change facadeLogSetup to try
buildNoFollowLogSetupCommand("/tmp/discord-facade.log", ...) and if it returns a
non-fatal EACCES/EPERM outcome fall back to creating or using a writable path
(e.g. mktemp in $TMPDIR or /dev/shm/discord-facade-XXXX) and set a single
variable (e.g. facadeLogPath) to that chosen path; then update facadeLaunch to
use that facadeLogPath for the exec redirection instead of the hardcoded
"/tmp/discord-facade.log" so the setup and launch always reference the same
writable log file.

In `@test/policies.test.ts`:
- Around line 798-830: rulesFor currently uses find so it only returns rules
from the first matching endpoint; change rulesFor to collect rules from all
endpoints for the host (use filter + flatMap or map+flat) so
nousRules/discordRules include every endpoint's rules; then tighten the discord
assertions by not only checking expect.arrayContaining for required mutation
rules but also asserting that no other write rules exist (e.g., collect all
rules with methods in ["PUT","PATCH","DELETE"] from discordRules and assert they
exactly match the allowed list and that none have path "/**"). Ensure you update
references to rulesFor, nousRules, and discordRules in the test accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0d2e0a64-bd87-444e-88bf-f34b42cacfad

📥 Commits

Reviewing files that changed from the base of the PR and between a797ab9 and 1ab7fdf.

📒 Files selected for processing (4)
  • agents/hermes/policy-additions.yaml
  • src/lib/agent/runtime.test.ts
  • src/lib/agent/runtime.ts
  • test/policies.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • agents/hermes/policy-additions.yaml

Comment thread src/lib/agent/runtime.ts Outdated
Comment thread src/lib/agent/runtime.ts Outdated
Comment thread test/policies.test.ts Outdated
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working Integration: Discord Use this label to identify Discord bot integration issues with NemoClaw. Integration: Hermes v0.0.38 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants