-
Notifications
You must be signed in to change notification settings - Fork 0
feat: changed Dockerfile to handle hermes in gateway mode #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b9044e4
50e9c2d
ba42948
406bd95
d6ff34c
628c7a1
2e9b247
a1bf965
42292d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,4 @@ __pycache__/ | |
| *.pyo | ||
| *.pyd | ||
| .Pythonservices/custom_ui_guardrail/data/ | ||
| *.whl | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,26 @@ DOMYN_CHANNEL_ID= | |
| # Canvas tool-list poll interval in seconds. Set to 0 to disable. Default 60. | ||
| # PLATFORM_TOOL_REFRESH_INTERVAL=60 | ||
|
|
||
| # --- Authorization (required for gateway mode) ------------------------------- | ||
| # In gateway mode hermes runs its own per-user authorization layer on top of | ||
| # the platform. Domyn already authenticates callers at the relay/API layer, | ||
| # so we typically delegate fully and accept any author arriving on the | ||
| # subscribed channel. Without this, unknown users get a pairing prompt: | ||
| # "Hi~ I don't recognize you yet! Here's your pairing code: …" | ||
| DOMYN_ALLOW_ALL_USERS=true | ||
|
|
||
| # Optional alternative: a comma-separated allowlist of Domyn author IDs | ||
| # (leave DOMYN_ALLOW_ALL_USERS unset if you use this). | ||
| # DOMYN_ALLOWED_USERS=user-id-1,user-id-2 | ||
|
|
||
| # --- Home channel (recommended) ---------------------------------------------- | ||
| # Without this, gateway-mode hermes posts a one-time | ||
| # "📬 No home channel is set for Domyn. Type /sethome …" | ||
| # notice into the first conversation it sees. The Domyn worker is already | ||
| # bound to one channel, so we just point the home target at it — cron | ||
| # results and broadcasts go back to the same channel. | ||
| DOMYN_HOME_CHANNEL=${DOMYN_CHANNEL_ID} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we move this things as defaults in the plugin instead of allowing the user to mess up with them?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wait how can we default domyn-channel-id? |
||
|
|
||
| # --- LLM provider (required by hermes itself) -------------------------------- | ||
| # Hermes needs at least one model provider. Defaults below target the | ||
| # vLLM gateway used by the example agents in `custom_samples`; swap for | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,32 +26,37 @@ RUN pip install --no-cache-dir \ | |
| # 3) domyn-agents — required by the gateway plugin for relay event models. | ||
| # Bundled as a wheel under ./wheels so the build doesn't need the private | ||
| # igenius PyPI index. | ||
| # `langchain-core` is pulled in because the `domyn` CLI eagerly imports the | ||
| # langgraph integration (needed by step 4's `domyn install-plugin` call). | ||
| COPY wheels/ wheels/ | ||
| RUN pip install --no-cache-dir wheels/domyn_agents-*.whl | ||
|
|
||
| # 4) hermes-platform-gateway plugin. Two installs are needed: | ||
| # - The pip package goes into site-packages so the plugin's | ||
| # `from hermes_platform_gateway.client import …` imports resolve. | ||
| # - The manifest + __init__.py are also dropped under $HERMES_HOME/plugins | ||
| # so hermes' discovery sees a `plugin.yaml` and calls `register(ctx)`. | ||
| COPY plugins/hermes-platform-gateway/ /opt/hermes-platform-gateway/ | ||
| RUN pip install --no-cache-dir /opt/hermes-platform-gateway && \ | ||
| mkdir -p ${HERMES_HOME}/plugins/hermes_platform_gateway && \ | ||
| cp /opt/hermes-platform-gateway/hermes_platform_gateway/*.py \ | ||
| ${HERMES_HOME}/plugins/hermes_platform_gateway/ && \ | ||
| cp /opt/hermes-platform-gateway/plugin.yaml \ | ||
| ${HERMES_HOME}/plugins/hermes_platform_gateway/ | ||
|
|
||
| # 5) hermes config: | ||
| # - point hermes' default model at the vLLM gateway (api_key + base_url + | ||
| # model come from env vars at startup via hermes' ${VAR} expansion); | ||
| # - opt-in the platform-gateway plugin — standalone-kind plugins are | ||
| # skipped unless explicitly listed under `plugins.enabled`. | ||
| COPY hermes-config.yaml ${HERMES_HOME}/config.yaml | ||
|
|
||
| # 6) Run hermes as a relay-attached worker. Stdin is closed in containers, | ||
| # so we drop into `hermes chat` non-interactively — the plugin's | ||
| # `on_session_start` hook will still fire, the WS subscriber stays open | ||
| # in a daemon thread, and AGENT_START events injected from the platform | ||
| # drive the conversation. | ||
| CMD ["hermes", "chat"] | ||
| RUN pip install --no-cache-dir wheels/domyn_agents-*.whl langchain-core | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does hermes require langchain? I am surprised
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's because of a dependency in domyn expose, i was planning to remove it after the domyn expose PR from gabriele was merged, to avoid useless merge conflicts |
||
|
|
||
| # 4) hermes-platform-gateway plugin. | ||
| # `domyn install-plugin` drops the vendored plugin source plus its | ||
| # `plugin.yaml` manifest directly into ${HERMES_HOME}/plugins/hermes_platform_gateway/, | ||
| # which is what hermes' on-disk plugin scan picks up at startup. No pip | ||
| # install or manual cp is needed — the plugin uses relative imports and | ||
| # `domyn-agents` is already on sys.path from step 3. | ||
| RUN domyn install-plugin --framework hermes | ||
|
|
||
| # 5) i18n catalog — upstream hermes' pip install does NOT package its | ||
| # locales/ directory, so every t() call falls back to the raw key | ||
| # (you'd see "gateway.approve.session_singular" rendered verbatim in | ||
| # chat replies to /approve). We drop our own minimal en.yaml next to | ||
| # the installed agent/ package so agent.i18n._locales_dir() finds it. | ||
| COPY locales/en.yaml /usr/local/lib/python3.11/site-packages/locales/en.yaml | ||
|
|
||
| # 6) hermes config template + entrypoint that materialises it. | ||
| # gateway mode reads ~/.hermes/config.yaml via read_raw_config() which | ||
| # does NOT expand ${VAR} references — so we resolve them at container | ||
| # start with entrypoint.sh and write the result into $HERMES_HOME. | ||
| COPY hermes-config.yaml /opt/hermes-config.template.yaml | ||
| COPY entrypoint.sh /usr/local/bin/entrypoint.sh | ||
| RUN chmod +x /usr/local/bin/entrypoint.sh | ||
|
|
||
| # 7) Run hermes in gateway mode. The gateway is the headless, multi-session | ||
| # runner — it doesn't open a TUI and doesn't need a TTY. AGENT_START | ||
| # events for any conversation_id arriving on the subscribed channel are | ||
| # routed by the gateway to a per-conversation hermes session. | ||
| ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] | ||
| CMD ["hermes", "gateway"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| #!/bin/sh | ||
| # hermes-platform-gateway entrypoint. | ||
| # | ||
| # hermes' gateway-mode config loader (gateway/run.py:_load_gateway_config → | ||
| # hermes_cli/config.read_raw_config) reads ~/.hermes/config.yaml RAW — | ||
| # without expanding ${VAR} references the way load_config() does. So we | ||
| # materialise the resolved config here before hermes starts. | ||
| # | ||
| # The image ships a template at /opt/hermes-config.template.yaml; we | ||
| # substitute env vars and write the result to $HERMES_HOME/config.yaml on | ||
| # every boot. Unconditional overwrite is intentional: this deployment | ||
| # treats hermes' config as ephemeral (Domyn is the source of truth for | ||
| # everything user-mutable). | ||
|
|
||
| set -e | ||
|
|
||
| : "${HERMES_HOME:=/root/.hermes}" | ||
| TEMPLATE=/opt/hermes-config.template.yaml | ||
| TARGET="$HERMES_HOME/config.yaml" | ||
|
|
||
| mkdir -p "$HERMES_HOME" | ||
|
|
||
| # Re-sync the platform-gateway plugin from the vendored source on every boot. | ||
| # $HERMES_HOME is a docker-compose named volume that persists across rebuilds, | ||
| # so anything written into it at image-build time gets shadowed by the volume's | ||
| # captured contents on subsequent runs. Re-running `domyn install-plugin --force` | ||
| # pulls the current vendored source out of the domyn-agents wheel and | ||
| # overwrites the plugin directory inside the volume, keeping it in sync with | ||
| # the image without requiring a `docker compose down -v`. | ||
| domyn install-plugin --framework hermes --force | ||
|
|
||
| python3 - <<'PY' | ||
| import os | ||
| import re | ||
| import sys | ||
|
|
||
| template_path = "/opt/hermes-config.template.yaml" | ||
| target_path = os.path.join(os.environ.get("HERMES_HOME", "/root/.hermes"), "config.yaml") | ||
|
|
||
| with open(template_path, encoding="utf-8") as f: | ||
| text = f.read() | ||
|
|
||
| def _sub(match): | ||
| name = match.group(1) | ||
| val = os.environ.get(name) | ||
| if val is None: | ||
| print(f"warning: ${{{name}}} is not set in the environment", file=sys.stderr) | ||
| return match.group(0) | ||
| return val | ||
|
|
||
| resolved = re.sub(r"\$\{([A-Z0-9_]+)\}", _sub, text) | ||
|
|
||
| with open(target_path, "w", encoding="utf-8") as f: | ||
| f.write(resolved) | ||
| PY | ||
|
|
||
| exec "$@" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,4 +9,37 @@ model: | |
|
|
||
| plugins: | ||
| enabled: | ||
| - hermes_platform_gateway | ||
| - hermes-platform-gateway | ||
|
|
||
| agent: | ||
| # The Domyn canvas is the source of truth for tools on this worker. Drop | ||
| # hermes' built-in web + browser toolsets so: | ||
| # 1. They don't shadow-conflict with canvas tools of the same name | ||
| # (e.g. canvas `web_search` vs built-in `web_search`). | ||
| # 2. The Chromium-not-installed warning at startup goes away. | ||
| # Anything the canvas wants to expose still arrives via the platform-gateway | ||
| # plugin's tool registration. | ||
| disabled_toolsets: | ||
| - web | ||
| - browser | ||
| # Stop hermes from injecting periodic "⏳ Still working…" progress | ||
| # messages into the chat. Default 180s; set 0 to disable. Without this | ||
| # users see "Still working..." every 3 minutes during long turns. | ||
| gateway_notify_interval: 0 | ||
| reasoning_effort: "medium" # empty = medium (default). Options: none, minimal, low, medium, high, xhigh (max) | ||
|
|
||
|
|
||
|
|
||
| display: | ||
| # How hermes handles a new message that arrives during an active turn. | ||
| # Default "interrupt" sends a visible "⚡ Interrupting current task…" | ||
| # status message — disruptive in a chat UI. "queue" silently FIFO-s | ||
| # the new message behind the current one; nothing visible until the | ||
| # current turn completes. "steer" injects as additional context. | ||
| busy_input_mode: queue | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this will conflict with stopping executions in flight, but we'll make some tests after merging this and rebasing the stop stuff |
||
| # Mid-turn narrative ("Ti aiuto a creare uno script…") is enabled. | ||
| # The adapter routes these to LLM_END events (not AGENT_END), so | ||
| # apps that treat AGENT_END as the turn-terminator stay correct — | ||
| # only the final response and slash-command replies emit AGENT_END. | ||
| # See ``DomynPlatformAdapter.send`` for the routing logic. | ||
| interim_assistant_messages: true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Minimal i18n catalog for the Domyn worker. | ||
| # | ||
| # Hermes' ``pip install`` from git doesn't package the ``locales/`` | ||
| # directory shipped in the repo, so the in-container i18n catalog is | ||
| # missing and every ``t()`` call falls back to the raw key (you'd see | ||
| # things like ``gateway.approve.session_singular`` rendered verbatim in | ||
| # chat). We bake this file in via the Dockerfile to backfill the keys | ||
| # hermes' gateway emits during normal slash-command operation. | ||
| # | ||
| # Scope is deliberately narrow — we ship only the keys that surface as | ||
| # user-facing text in this deployment (approval / deny flows, draining, | ||
| # expired-approval notices, the bundled ``approval.*`` prompt strings). | ||
| # Other ``t()`` calls hermes makes will still fall back to the raw key | ||
| # until upstream fixes its packaging or we expand this file. | ||
|
|
||
| approval: | ||
| dangerous_header: "⚠️ DANGEROUS COMMAND: {description}" | ||
| choose_long: " [o]nce | [s]ession | [a]lways | [d]eny" | ||
| choose_short: " [o]nce | [s]ession | [d]eny" | ||
| prompt_long: " Choice [o/s/a/D]: " | ||
| prompt_short: " Choice [o/s/D]: " | ||
| timeout: " ⏱ Timeout - denying command" | ||
| allowed_once: " ✓ Allowed once" | ||
| allowed_session: " ✓ Allowed for this session" | ||
| allowed_always: " ✓ Added to permanent allowlist" | ||
| denied: " ✗ Denied" | ||
| cancelled: " ✗ Cancelled" | ||
| blocklist_message: "This command is on the unconditional blocklist and cannot be approved." | ||
|
|
||
| gateway: | ||
| approval_expired: "⚠️ Approval expired (agent is no longer waiting). Ask the agent to try again." | ||
| draining: "⏳ Draining {count} active agent(s) before restart..." | ||
| goal_cleared: "✓ Goal cleared." | ||
| no_active_goal: "No active goal." | ||
| config_read_failed: "⚠️ Could not read config.yaml: {error}" | ||
| config_save_failed: "⚠️ Could not save config: {error}" | ||
|
|
||
| # /approve replies — key shape is gateway.approve.{choice}_{plural} | ||
| # where choice ∈ {once, session, always}, plural ∈ {singular, plural}. | ||
| # See gateway/run.py around line 13455. | ||
| approve: | ||
| no_pending: "No pending command to approve." | ||
| once_singular: "✅ Command approved. The agent is resuming..." | ||
| once_plural: "✅ {count} commands approved. The agent is resuming..." | ||
| session_singular: "✅ Command approved (pattern approved for this session). The agent is resuming..." | ||
| session_plural: "✅ {count} commands approved (pattern approved for this session). The agent is resuming..." | ||
| always_singular: "✅ Command approved (pattern approved permanently). The agent is resuming..." | ||
| always_plural: "✅ {count} commands approved (pattern approved permanently). The agent is resuming..." | ||
|
|
||
| # /deny replies — symmetric shape to /approve. | ||
| deny: | ||
| no_pending: "No pending command to deny." | ||
| stale: "❌ Command denied (approval was stale)." | ||
| denied_singular: "❌ Command denied. The agent is resuming with a BLOCKED result." | ||
| denied_plural: "❌ {count} commands denied. The agent is resuming with BLOCKED results." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this ties back to the comments I made here.
https://github.com/igeniusai/domyn-agents/pull/387/changes
But also about this, do we really want the developer to set this? What is the expected journey for the dev?