Repository navigation
fix: populate <RUNTIME_SERVICES> in static builds so automations work - #1125
Conversation
The agent's <RUNTIME_SERVICES> system-prompt block is built from VITE_RUNTIME_SERVICES_INFO, which the dev launchers set at build time. Static builds (the Docker image and the published binary) run `npm run build` without it, so the block is dropped and the agent does not know how to reach the local automation backend — it falls back to the cloud API (app.all-hands.dev) and automation creation fails. Inject the info at serve time, mirroring the existing --session-api-key path: - scripts/static-server.mjs: new --runtime-services-info flag injects the JSON as window.__AGENT_CANVAS_RUNTIME_SERVICES_INFO__ - src/api/agent-server-adapter.ts: parseRuntimeServicesInfo() falls back to that window global when the env var is empty - docker/entrypoint.sh: builds the JSON from the sandbox-facing service URLs (AGENT_SERVER_URL, AUTOMATION_BASE_URL) and passes it to the static server(s) Refs #1098
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Replace the inline JSON building in docker/entrypoint.sh with a single source of truth for the <RUNTIME_SERVICES> shape. buildRuntimeServicesInfo now lives in scripts/runtime-services-info.mjs (moved out of dev-safe.mjs, which re-exports it for back-compat) and gains: - optional full-URL overrides (agentServerUrl, automation.url) so the container can pass its runtime-resolved AGENT_SERVER_URL / AUTOMATION_BASE_URL and use 127.0.0.1 (avoiding IPv6 loopback) instead of build-time ports, and - a CLI entrypoint so docker/entrypoint.sh emits the JSON by running the same builder the dev stack uses, rather than a hand-rolled node -e blob. The Dockerfile ships the new dependency-free module into the image.
|
✅ Review complete. This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here. |
all-hands-bot
left a comment
There was a problem hiding this comment.
Code Review
Thank you for this fix! The approach is well-thought-out and the PR is well-documented. Here is my review:
Overall Assessment
What this PR does: Fixes the missing <RUNTIME_SERVICES> system-prompt block in static builds (Docker/published binary) so that automations can work correctly.
Architecture: The fix follows a clean separation of concerns:
runtime-services-info.mjs— single source of truth for the builderstatic-server.mjs— dumb injector that adds the JSON to index.html at serve timeagent-server-adapter.ts— dumb consumer that reads the infoentrypoint.sh— runs the CLI with runtime URLs
Strengths
-
Excellent documentation: The PR description is thorough, explaining the root cause, the fix strategy, and the reasoning behind each design decision.
-
Clean refactoring: Moving
buildRuntimeServicesInfoto its own module (runtime-services-info.mjs) makes the code more modular and allows the Docker entrypoint to use it as a CLI. -
Robust error handling: The code properly validates inputs (e.g., throws when
agentServerPortoragentServerUrlis missing) to prevent baking:undefinedinto the system prompt. -
Comprehensive tests: The new test coverage is excellent — unit tests for the builder, CLI tests for the entrypoint path, and integration tests for the injection pipeline.
-
Good backward compatibility: The re-export in
dev-safe.mjsensures existing importers continue to work. -
Consistent patterns: The implementation mirrors the existing
--session-api-key/--auth-requiredinjection pattern, making it intuitive for developers familiar with the codebase.
Suggestions (non-blocking)
-
TypeScript typing for the new module: While the new
runtime-services-info.mjsmodule is JavaScript (appropriate for a CLI), you might consider adding JSDoc type annotations for the options object to match the TypeScript interfaces elsewhere. The JSDoc is already present, which is good. -
Consider edge case in
parseArgs: The CLI parsing handles empty strings for--agent-server-urland--automation-urlby converting them toundefined, which is correct. However, the behavior when--agent-server-urlis followed by another flag (e.g.,--agent-server-url --mode) might be worth testing. Currently it would treat--modeas the URL value, which is then converted toundefined. -
Docker multi-stage build optimization: The new file
runtime-services-info.mjsis copied unconditionally. If the file is only needed for the final stage, this is fine. But if it's only needed when building with automation support, you might consider conditional copying (though this may be over-engineering for a single small file).
Minor Observations
-
Line in
agent-server-adapter.ts: The error message in thecatchblock says "misconfigured env var" but could also be "misconfigured env var or injected value" since the fallback path also exists. -
Test file structure: The tests are well-organized with clear descriptions. The test names like
"falls back to window.__AGENT_CANVAS_RUNTIME_SERVICES_INFO__ when the env var is unset (static builds)"clearly describe the scenario being tested.
Conclusion
This is a solid, well-tested fix. The architecture is clean, the code is well-documented, and the tests provide good coverage. The PR successfully addresses the automation failure described in issue #1098 without introducing unnecessary complexity.
Recommendation: Approve
This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation
|
@OpenHands can you modify the mock llm e2e tests and confirm that the runtime info is correctly constructed and included for conversations. maybe for the automations e2e test add this additional check |
|
I'm on it! malhotra5 can track my progress at all-hands.dev |
…tion e2e - startStaticFrontend() in dev-with-automation.mjs now builds the runtime-services info JSON via buildAutomationRuntimeServicesInfo() and passes it to static-server.mjs via --runtime-services-info. Without this, the npm binary / dev:static path served pre-built frontends that never populated the agent's <RUNTIME_SERVICES> system-prompt block (the Docker entrypoint already did this). - The mock-LLM automation e2e test now verifies that the <RUNTIME_SERVICES> block is present in the system messages sent to the LLM, and that it includes the expected service entries (Agent Server, Automation backend, /api/automation). Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
❌ Mock-LLM E2E Tests24/25 passed · 1 failed Commit:
🔍 Failure details (1)❌ mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to Anthropic Claude OpusPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
❌ Mock-LLM Docker E2E Test Results19/25 passed · 1 failed · 5 skipped Commit:
🔍 Failure details (1)❌ mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to Anthropic Claude OpusPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
|
The e2e test failure is unrelated to this PR so I'm going to approve it |
|
| Status | Test | Duration |
|---|
Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)
❌ Mock-LLM E2E Tests24/25 passed · 1 failed Commit:
🔍 Failure details (1)❌ mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to Anthropic Claude OpusPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
❌ Mock-LLM E2E Tests24/25 passed · 1 failed Commit:
🔍 Failure details (1)❌ mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to Anthropic Claude OpusPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
❌ Mock-LLM Docker E2E Test Results19/25 passed · 1 failed · 5 skipped Commit:
🔍 Failure details (1)❌ mock-llm-onboarding-regressions.spec.ts › onboarding recent regressions › defaults the LLM setup step to Anthropic Claude OpusPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
✅ Mock-LLM E2E Tests25/25 passed Commit:
Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
🔶 Mock-LLM Docker E2E Test Results20/25 passed · 5 skipped Commit:
Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
|
🚀 Released in v1.2.0. |
✅ Mock-LLM E2E Tests25/25 passed Commit:
Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
🔶 Mock-LLM Docker E2E Test Results20/25 passed · 5 skipped Commit:
Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
📸 Snapshot Test ReportWarning Snapshot comparison step crashed (timeout, OOM, or runner error) — diff results below may be incomplete or absent. ✅ No baseline found on main — all 73 snapshots are new and will become the baseline once this PR merges.
🆕 New snapshots (73)These snapshots have no baseline on main and will become the new baseline once this PR merges.
|









































































Problem
Reported in #2387: in Docker (and the npm/binary install) the agent fails to create automations — it asks for an
OPENHANDS_API_KEYor falls back to the cloud service (app.all-hands.dev), which rejects the local key.Root cause
The agent's
<RUNTIME_SERVICES>system-prompt block (which tells the agent how to reach the local automation backend) is built fromVITE_RUNTIME_SERVICES_INFO. That env var is only set at build time by the dev launchers (scripts/dev-*.mjs/static-build.mjs). Static builds run plainnpm run build(docker/Dockerfile), so the var is empty,parseRuntimeServicesInfo()returnsnull, and the block is omitted.Crucially, the dev trick can't be reused at
docker buildtime: the ports/URLs (PORT,AGENT_SERVER_URL,AUTOMATION_BASE_URL, …) are runtime config, overridable atdocker run, and the URLs are written from the agent's point of view (container loopback), not the browser's. So the info has to be supplied at container start, not baked into the image.Fix
Inject the info at serve time, mirroring the existing
--session-api-key/--auth-requiredinjection already used for the published binary:scripts/runtime-services-info.mjs(new) — single source of truth for the block's shape. HoldsbuildRuntimeServicesInfo()(moved out ofdev-safe.mjs, which re-exports it for back-compat), extended with optional full-URL overrides, plus a small CLI so the entrypoint can emit the JSON with the same builder the dev stack uses.scripts/static-server.mjs— new--runtime-services-info <json>flag injects the JSON aswindow.__AGENT_CANVAS_RUNTIME_SERVICES_INFO__(dumb injector; doesn't know the shape).src/api/agent-server-adapter.ts—parseRuntimeServicesInfo()falls back to that window global when the env var is empty (same idiom asgetBakedSessionApiKey()).docker/entrypoint.sh— runs the CLI with the sandbox-facing URLs it already exports (AGENT_SERVER_URL,AUTOMATION_BASE_URL,127.0.0.1to avoid IPv6 loopback) and passes the result to the static server(s).docker/Dockerfileships the new module.Responsibilities stay clean: one builder knows the shape, static-server is a dumb injector, the adapter is a dumb consumer.
Scope
This fixes the missing
<RUNTIME_SERVICES>block — the automation-creation failure that is the subject of #2387. The other observations in that issue (missingIMPORTANT/CURRENT_DATETIMEsections and skills not loading) originate in the agent-server/SDK base prompt, not the frontend, and are out of scope here.Testing
npm run lint(typecheck + eslint + prettier onsrc) — cleanvitest run— 2977 passing (incl. newruntime-services-info.test.ts)agent-server-adapter.test.ts);--runtime-services-infoparse + injection (static-server.test.ts); URL-override builder + CLI end-to-end (runtime-services-info.test.ts)RuntimeServicesInfo-shaped object that round-trips through the browser injection →JSON.parseNot yet validated
A live Docker build + automation-creation smoke test (logic, parsing, and round-trip are validated; an actual container run is not).
🐳 Docker images for this PR
• GHCR package: https://github.com/OpenHands/agent-canvas/pkgs/container/agent-canvas
ghcr.io/openhands/agent-canvasghcr.io/openhands/agent-server:1.24.0-pythonopenhands-automation==1.0.0a620cbef97e46f0705c8c4fc53ae84aa68ac502b9fPull (multi-arch manifest)
# Multi-arch manifest — Docker automatically pulls the correct architecture docker pull ghcr.io/openhands/agent-canvas:sha-20cbef9Run
All tags pushed for this build
About Multi-Architecture Support
sha-20cbef9) is a multi-arch manifest supporting both amd64 and arm64sha-20cbef9-amd64) are also available if needed