File size: 8,558 Bytes
c0af099 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | /**
* Single source of truth for the `<RUNTIME_SERVICES>` block.
*
* Builds a structured description of the services that are reachable from
* inside the agent's sandbox. Agent Canvas backend-serving processes attach it
* to `/server_info.runtime_services`; the frontend renders that backend value
* into `AgentContext.system_message_suffix`, so the agent sees a
* `<RUNTIME_SERVICES>` block listing what's available without having to probe.
*
* Two callers share this one definition:
* - the dev launchers (scripts/dev-*.mjs), which know the stack as a set of
* ports and pass the result to ingress/static-server for `/server_info`;
* - docker/entrypoint.sh, which runs this file as a CLI (see the bottom of
* this module) because in a container the URLs are *runtime* config β the
* ports and base URLs are overridable at `docker run` and therefore cannot
* be baked into the image at build time. The JSON it prints is passed to
* scripts/static-server.mjs and exposed through `/server_info`.
*
* URLs are written from the *agent's* point of view (i.e. as the agent should
* curl/fetch them from inside its sandbox), which is deliberately not the
* browser's point of view.
*/
import process from "node:process";
import { pathToFileURL } from "node:url";
/**
* @param {object} options
* @param {string} [options.mode] - Human-readable mode label (e.g. "dev:safe").
* @param {string} [options.agentHostAlias="localhost"] - Hostname the agent
* uses to reach host-side services (ingress, frontend, port-derived
* automation). Also surfaced as `agent_host_alias`.
* @param {number} [options.agentServerPort] - Port the agent-server listens on.
* Used to derive the agent_server URL when `agentServerUrl` is not given.
* @param {string} [options.agentServerUrl] - Explicit agent_server URL, from
* the agent's POV. Takes precedence over `agentServerPort`; used by the
* Docker image, which serves over `127.0.0.1` to avoid IPv6 loopback issues
* and honors an overridable `AGENT_SERVER_URL`. One of `agentServerUrl` /
* `agentServerPort` is required (else the URL would be `:undefined`).
* @param {number} [options.ingressPort] - Ingress port (omit if no ingress).
* @param {number} [options.frontendPort] - Frontend port (Vite dev server
* or static-file server). Omit if no frontend is exposed.
* @param {number} [options.vitePort] - Deprecated alias for `frontendPort`,
* accepted for backward compat with older launchers. Remove after one release.
* @param {"vite"|"static"} [options.frontendKind="vite"] - Whether the
* frontend port hosts Vite or a static build. Only affects the description.
* @param {object} [options.automation] - Automation backend info. Skipped
* entirely unless `.url` or `.port` is provided, so passing `{}` is safe.
* @param {string} [options.automation.url] - Explicit automation base URL, from
* the agent's POV. Takes precedence over `.port`; used by the Docker image to
* honor an overridable `AUTOMATION_BASE_URL`.
* @param {number} [options.automation.port] - Automation backend port (used to
* derive the base URL when `.url` is not given).
* @param {string} [options.automation.apiPrefix="/api/automation"] - Path
* prefix all automation routes are mounted under.
* @param {string} [options.automation.authEnvVar="OPENHANDS_AUTOMATION_API_KEY"]
* - Env var holding the API key.
* @returns {object} A JSON-serializable runtime services info object.
*/
export function buildRuntimeServicesInfo(options) {
const {
mode,
agentHostAlias = "localhost",
agentServerPort,
agentServerUrl,
ingressPort,
// Accept legacy `vitePort` for one release so external callers keep working.
vitePort,
frontendPort = vitePort,
frontendKind = "vite",
automation,
} = options;
// Prefer an explicit URL (containers reach the agent-server over a specific
// host/scheme), else derive it from the port. From the agent's POV the
// agent-server it's *inside* is on the loopback host, regardless of where
// the host machine is.
const agentServerUrlResolved =
agentServerUrl ??
(agentServerPort != null ? `http://localhost:${agentServerPort}` : null);
if (!agentServerUrlResolved) {
// Without this the URL becomes `http://localhost:undefined` and ends up
// verbatim in the agent's system prompt, which is worse than failing fast.
throw new Error(
"buildRuntimeServicesInfo: agentServerPort or agentServerUrl is required " +
"(otherwise the agent_server URL would be `http://localhost:undefined`).",
);
}
const services = {
agent_server: {
description:
"The OpenHands Agent Server this agent is running inside. " +
"Tool calls (terminal, file_editor, browser, etc.) execute here.",
url_from_agent: agentServerUrlResolved,
},
};
if (ingressPort !== undefined) {
services.ingress = {
description:
"Unified entry point. Routes /api/automation/* to the automation " +
"backend, /api/* and /sockets to the agent-server, and /* to the " +
"frontend.",
url_from_agent: `http://${agentHostAlias}:${ingressPort}`,
};
}
if (frontendPort !== undefined) {
services.frontend = {
kind: frontendKind,
description:
frontendKind === "static"
? "Static-file server hosting the agent-canvas production build."
: "Vite dev server hosting the agent-canvas frontend.",
url_from_agent: `http://${agentHostAlias}:${frontendPort}`,
};
}
// Prefer an explicit base URL, else derive from the port. Require one of the
// two so we don't bake `:undefined` into the URL when the caller passes
// `automation: {}`.
const automationBaseUrl =
automation?.url ??
(automation?.port != null
? `http://${agentHostAlias}:${automation.port}`
: null);
if (automationBaseUrl) {
const apiPrefix = automation.apiPrefix ?? "/api/automation";
const authEnvVar = automation.authEnvVar ?? "OPENHANDS_AUTOMATION_API_KEY";
services.automation = {
description:
"OpenHands Automations service. All routes are mounted under " +
`'${apiPrefix}'. Authenticate with header ` +
`'X-Session-API-Key: $${authEnvVar}'.`,
url_from_agent: automationBaseUrl,
api_prefix: apiPrefix,
docs_url: `${automationBaseUrl}${apiPrefix}/docs`,
openapi_url: `${automationBaseUrl}${apiPrefix}/openapi.json`,
auth_env_var: authEnvVar,
};
}
return {
mode,
agent_host_alias: agentHostAlias,
services,
};
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// CLI β used by docker/entrypoint.sh to emit the JSON at container startup.
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function parseArgs(argv) {
const options = { automation: {} };
for (let i = 0; i < argv.length; i++) {
const flag = argv[i];
switch (flag) {
case "--mode":
options.mode = argv[++i];
break;
case "--agent-host-alias":
options.agentHostAlias = argv[++i];
break;
case "--agent-server-url":
options.agentServerUrl = argv[++i] || undefined;
break;
case "--automation-url":
options.automation.url = argv[++i] || undefined;
break;
case "--automation-api-prefix":
options.automation.apiPrefix = argv[++i];
break;
case "--automation-auth-env":
options.automation.authEnvVar = argv[++i];
break;
default:
throw new Error(`Unknown flag: ${flag}`);
}
}
// Omit the automation entry entirely when no URL was supplied, rather than
// advertising a backend the agent cannot reach.
if (!options.automation.url) delete options.automation;
return options;
}
const isMainModule =
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (isMainModule) {
try {
const options = parseArgs(process.argv.slice(2));
process.stdout.write(JSON.stringify(buildRuntimeServicesInfo(options)));
} catch (err) {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
}
}
|