diff --git a/apps/daemon/internal/cli/connect_environment.go b/apps/daemon/internal/cli/connect_environment.go
index de92018e..ec8ab659 100644
--- a/apps/daemon/internal/cli/connect_environment.go
+++ b/apps/daemon/internal/cli/connect_environment.go
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io"
- "net"
"net/http"
"net/url"
"os"
@@ -42,10 +41,6 @@ func environmentBase(remote string) (string, error) {
case "wss":
u.Scheme = "https"
case "ws":
- ip := net.ParseIP(u.Hostname())
- if u.Hostname() != "localhost" && (ip == nil || !ip.IsLoopback()) {
- return "", errors.New("connect: Environment remote_url requires TLS outside loopback")
- }
u.Scheme = "http"
default:
return "", errors.New("connect: Environment remote_url must use ws or wss")
diff --git a/apps/daemon/internal/cli/connect_environment_test.go b/apps/daemon/internal/cli/connect_environment_test.go
index 9bea081d..dc9bd381 100644
--- a/apps/daemon/internal/cli/connect_environment_test.go
+++ b/apps/daemon/internal/cli/connect_environment_test.go
@@ -16,13 +16,13 @@ import (
)
func TestEnvironmentConnectionURL(t *testing.T) {
- for _, valid := range []string{"wss://runtime.example/api/v1/agent-daemon/ws", "ws://127.0.0.1:123/api/v1/agent-daemon/ws", "ws://[::1]:123/api/v1/agent-daemon/ws"} {
+ for _, valid := range []string{"wss://runtime.example/api/v1/agent-daemon/ws", "ws://127.0.0.1:123/api/v1/agent-daemon/ws", "ws://[::1]:123/api/v1/agent-daemon/ws", "ws://runtime.example/api/v1/agent-daemon/ws"} {
base, err := environmentBase(valid)
if err != nil || !strings.HasSuffix(base, "/api/v1") {
t.Fatalf("valid URL rejected: %v", err)
}
}
- for _, invalid := range []string{"ws://runtime.example/api/v1/agent-daemon/ws", "https://runtime.example/api/v1/agent-daemon/ws", "wss://secret@runtime.example/api/v1/agent-daemon/ws", "wss://runtime.example/api/v1/agent-daemon/ws?secret=value", "wss://runtime.example/api/v1/agent-daemon/ws#fragment", "wss://runtime.example/api/v1/agent-daemon/ws/", "wss://runtime.example/api%2fv1/agent-daemon/ws"} {
+ for _, invalid := range []string{"https://runtime.example/api/v1/agent-daemon/ws", "wss://secret@runtime.example/api/v1/agent-daemon/ws", "wss://runtime.example/api/v1/agent-daemon/ws?secret=value", "wss://runtime.example/api/v1/agent-daemon/ws#fragment", "wss://runtime.example/api/v1/agent-daemon/ws/", "wss://runtime.example/api%2fv1/agent-daemon/ws"} {
if _, err := environmentBase(invalid); err == nil || strings.Contains(err.Error(), "secret") {
t.Fatal("invalid URL accepted or disclosed")
}
diff --git a/apps/web/e2e/nodes.spec.ts b/apps/web/e2e/nodes.spec.ts
index bc5ea436..721bcf6a 100644
--- a/apps/web/e2e/nodes.spec.ts
+++ b/apps/web/e2e/nodes.spec.ts
@@ -125,7 +125,7 @@ test("issues no command before the installation is read, for a loopback public U
await page.unroute("**/core/v1/installation");
await add.getByRole("button", { name: "Try again" }).click();
// Nodes on other machines can't reach a loopback public_url.
- await expect(add.getByRole("status")).toHaveText("Set a public HTTPS address before adding nodes.");
+ await expect(add.getByRole("status")).toHaveText("Set a public address other machines can reach before adding nodes.");
await expect(add.getByRole("button", { name: "Generate command" })).toHaveCount(0);
await add.getByRole("button", { name: "Close dialog" }).click();
await expect(page.getByRole("button", { name: "Add node", exact: true })).toBeDisabled();
diff --git a/apps/web/e2e/overview-readiness.spec.ts b/apps/web/e2e/overview-readiness.spec.ts
index 55d426b2..5f22542d 100644
--- a/apps/web/e2e/overview-readiness.spec.ts
+++ b/apps/web/e2e/overview-readiness.spec.ts
@@ -82,7 +82,7 @@ test("local-only address links to System on other pages and blocks Add node", as
const notice = page.getByRole("status", { name: "Public address needs attention" });
await expect(notice.getByRole("button", { name: "Review the public address" })).toBeVisible();
await expect(page.locator(".getting-started-step").first()).toContainText("To do");
- await expect(page.locator(".getting-started-step").first()).toContainText("Configure HTTPS");
+ await expect(page.locator(".getting-started-step").first()).toContainText("Set a public address");
await page.getByRole("button", { name: "Nodes", exact: true }).click();
await expect(notice).toBeVisible();
await expect(page.getByRole("button", { name: "Add node", exact: true })).toBeDisabled();
@@ -102,7 +102,7 @@ for (const language of ["en", "zh-CN"] as const) {
await page.getByRole("menuitemradio", { name: "简体中文" }).click();
}
await expect(page.locator(".overview-activity .error-state")).toContainText(language === "en" ? "Could not read the data" : "无法读取数据");
- await expect(page.locator(".installation-notice")).toContainText(language === "en" ? "Set a public HTTPS address before connecting" : "连接外部应用和节点前");
+ await expect(page.locator(".installation-notice")).toContainText(language === "en" ? "Set a public address other machines can reach" : "连接外部应用和节点前");
if (language === "zh-CN") await expect(page.locator("body")).not.toContainText("Core request failed");
await expect(page.getByRole("article").first()).toContainText(language === "en" ? "Down" : "不可用");
for (const width of [1280, 1440]) {
@@ -174,6 +174,6 @@ test("installation failure cannot complete onboarding and its retry reveals loca
await page.unroute("**/core/v1/installation");
await step.getByRole("button", { name: "Retry", exact: true }).click();
await expect(step).toContainText("To do");
- await expect(step).toContainText("Configure HTTPS");
+ await expect(step).toContainText("Set a public address");
await expect(page.locator(".installation-notice")).toBeVisible();
});
diff --git a/apps/web/e2e/public-url.spec.ts b/apps/web/e2e/public-url.spec.ts
index 2fd90006..14dda421 100644
--- a/apps/web/e2e/public-url.spec.ts
+++ b/apps/web/e2e/public-url.spec.ts
@@ -28,7 +28,7 @@ test("explains an E2B rejection in the wizard, with a link to domain setup", asy
await selectFixtureE2BBuild(page);
await page.getByRole("button", { name: "Next" }).click();
const address = page.getByRole("definition").filter({ hasText: "http://127.0.0.1:8091" });
- await expect(address).toContainText("Configure HTTPS in System");
+ await expect(address).toContainText("Set a public address before connecting remote nodes");
await page.getByRole("button", { name: "Save configuration" }).click();
const rejection = page.locator(".wizard-rejection");
await expect(rejection).toContainText("E2B sandboxes need a public HTTPS address.");
diff --git a/apps/web/src/components/InstallationNotice.test.tsx b/apps/web/src/components/InstallationNotice.test.tsx
index 84339b85..68efdcef 100644
--- a/apps/web/src/components/InstallationNotice.test.tsx
+++ b/apps/web/src/components/InstallationNotice.test.tsx
@@ -13,7 +13,7 @@ describe("local-only installation notice", () => {
it("links to the public address without exposing installer commands", () => {
const html = renderToStaticMarkup();
expect(html).toContain("Review the public address");
- expect(html).toContain("Set a public HTTPS address before connecting applications and nodes");
+ expect(html).toContain("Set a public address other machines can reach before connecting");
expect(html).not.toContain("config.json");
expect(html).not.toContain("oac apply");
});
diff --git a/apps/web/src/features/sandbox/NodeCleanupDialog.tsx b/apps/web/src/features/sandbox/NodeCleanupDialog.tsx
index 9f4815a3..fcd1bbf3 100644
--- a/apps/web/src/features/sandbox/NodeCleanupDialog.tsx
+++ b/apps/web/src/features/sandbox/NodeCleanupDialog.tsx
@@ -45,7 +45,7 @@ export function NodeCleanupDialog({ cleanup, open, onClose }: { cleanup: NodeCle
: cleanup && !sourceUrl ?
{join(stays, installation.data?.local_only && installation.data.public_url
? t("Other machines can't reach this installation's public URL, {{url}}, so no uninstall command can be given.", { url: installation.data.public_url })
- : t("An uninstall command needs an HTTPS public URL that other machines can reach, and this installation has none."))}
+ : t("An uninstall command needs a public URL that other machines can reach, and this installation has none."))}
: cleanup ?
{t("{{name}} is removed from Core. To remove its service and files from the host, run:", { name: cleanup.name })}
diff --git a/apps/web/src/features/sandbox/NodeEnrollment.tsx b/apps/web/src/features/sandbox/NodeEnrollment.tsx
index 3148bc2e..bd9cab55 100644
--- a/apps/web/src/features/sandbox/NodeEnrollment.tsx
+++ b/apps/web/src/features/sandbox/NodeEnrollment.tsx
@@ -43,7 +43,7 @@ const DEFAULT_RETAINED = "8";
* sudo (or directly as root), which installs the node as a system service.
* The log hint names that system service. No command is issued until the installation
* is read: one whose public URL other machines can't use (loopback, as
- * `local_only` says, or not HTTPS), an unreadable one, or a console that
+ * `local_only` says), an unreadable one, or a console that
* reports no node files for the deployment's provider (`node_artifacts`) says
* so instead. Each opening, and each return to the window while open, reads
* the installation and the console again, so a fix on the Core host shows
@@ -103,7 +103,7 @@ export function NodeEnrollment({ client, consoleConfig, deployment, nodes, open,
: installation.data === undefined
? installation.isError ? { text: t("The installation couldn't be read, so no command can be issued."), failed: true } : { text: t("Checking this installation's public URL…") }
: !publicUrl
- ? { text: t("Set a public HTTPS address before adding nodes.") }
+ ? { text: t("Set a public address other machines can reach before adding nodes.") }
: !nodeFilesAvailable(consoleConfig, deployment.provider)
? { text: t("This console has no node files for {{provider}}. Install Core from the offline bundle, or add the release artifacts and rerun ./install.sh.", { provider: backend }) }
: null;
diff --git a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx
index 70619c60..d9ffce98 100644
--- a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx
+++ b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx
@@ -378,7 +378,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
{address ? {address} : "—"}
{t("Managed in System")}
- {installation.data?.local_only ? {t("Configure HTTPS in System before connecting remote nodes or E2B sandboxes.")} : null}
+ {installation.data?.local_only ? {t("Set a public address before connecting remote nodes; E2B sandboxes need an HTTPS one.")} : null}
diff --git a/apps/web/src/features/sandbox/core-origin.test.ts b/apps/web/src/features/sandbox/core-origin.test.ts
index d6806825..36232bab 100644
--- a/apps/web/src/features/sandbox/core-origin.test.ts
+++ b/apps/web/src/features/sandbox/core-origin.test.ts
@@ -1,20 +1,11 @@
import { describe, expect, it } from "vitest";
-import { httpsOrigin, nodeSourceUrl } from "./core-origin";
-
-describe("HTTPS origin", () => {
- it.each([
- ["https://core.example.com", "https://core.example.com"],
- [" https://core.example.com:443/ ", "https://core.example.com:443"],
- ["https://10.74.84.167:18443", "https://10.74.84.167:18443"],
- ])("keeps %s as written", (input, expected) => expect(httpsOrigin(input)).toBe(expected));
- it.each(["", "/v1", "http://core.example", "http://localhost:8080", "https://core.example/v1", "https://user:secret@core.example", "https://@core.example", "https://core.example?", "https://core.example#", "https://core.example//", "https://core.example\\path", "https://co\nre.example", "file://core.example"])("rejects %s", (input) => expect(httpsOrigin(input)).toBeNull());
-});
+import { nodeSourceUrl } from "./core-origin";
describe("node command source", () => {
- it("is the installation's public URL, never a loopback, missing or plain HTTP one", () => {
+ it("is the installation's public URL unless only the Core host reaches it", () => {
expect(nodeSourceUrl({ public_url: "https://core.example.com:8443", local_only: false })).toBe("https://core.example.com:8443");
- expect(nodeSourceUrl({ public_url: "https://127.0.0.1:8091", local_only: true })).toBeNull();
+ expect(nodeSourceUrl({ public_url: "http://10.0.0.5:8080", local_only: false })).toBe("http://10.0.0.5:8080");
+ expect(nodeSourceUrl({ public_url: "http://localhost:8080", local_only: true })).toBeNull();
expect(nodeSourceUrl({ public_url: null, local_only: false })).toBeNull();
- expect(nodeSourceUrl({ public_url: "http://core.example.com", local_only: false })).toBeNull();
});
});
diff --git a/apps/web/src/features/sandbox/core-origin.ts b/apps/web/src/features/sandbox/core-origin.ts
index 091a2705..dea95321 100644
--- a/apps/web/src/features/sandbox/core-origin.ts
+++ b/apps/web/src/features/sandbox/core-origin.ts
@@ -1,26 +1,13 @@
import type { CoreInstallation } from "@oac/agents-client";
-import { isValidDirectCoreBaseUrl } from "../../lib/connection";
-
-/**
- * The value itself when it is an HTTPS origin: no path, query, fragment or
- * credentials; a single trailing slash is dropped. Otherwise null. It is kept
- * as written, not normalized, so an explicit port such as :443 stays exactly
- * as Core reports it.
- */
-export function httpsOrigin(value: string): string | null {
- const candidate = value.trim().replace(/\/$/, "");
- return /^https:\/\/[^/?#\\\s@]+$/i.test(candidate) && isValidDirectCoreBaseUrl(candidate) ? candidate : null;
-}
-
/**
* Where the node commands download the installer, and the `--source-url` they
* pass it: the installation's public URL, whose reverse proxy sends
* `/node-install/*` to this console. Unlike the browser's address, it is the
- * same from every machine. Null when other machines can't use it: loopback
- * (`local_only`), missing, or not an HTTPS origin.
+ * same from every machine. Core accepts only origins nodes may use, so it is
+ * null only when other machines can't reach it (`local_only`) or it is missing.
*/
export function nodeSourceUrl(installation: Pick): string | null {
- if (installation.local_only || !installation.public_url) return null;
- return httpsOrigin(installation.public_url);
+ if (installation.local_only) return null;
+ return installation.public_url;
}
diff --git a/apps/web/src/i18n/locales/en/common.ts b/apps/web/src/i18n/locales/en/common.ts
index 0095357c..edb2758c 100644
--- a/apps/web/src/i18n/locales/en/common.ts
+++ b/apps/web/src/i18n/locales/en/common.ts
@@ -10,7 +10,7 @@ export const common = {
},
installationNotice: {
title: "Public address needs attention",
- body: "Set a public HTTPS address before connecting applications and nodes from other machines.",
+ body: "Set a public address other machines can reach before connecting their applications and nodes.",
configure: "Review the public address",
addBlocked: "Add node is unavailable while the public address is local only.",
},
diff --git a/apps/web/src/i18n/locales/en/keys.ts b/apps/web/src/i18n/locales/en/keys.ts
index 9f02e263..e61ba480 100644
--- a/apps/web/src/i18n/locales/en/keys.ts
+++ b/apps/web/src/i18n/locales/en/keys.ts
@@ -107,8 +107,8 @@ export const keys = {
copyFailed: "Select the text and copy it manually.",
loading: "Reading the API address",
failed: "The API address couldn't be read.",
- localOnly: "For access from other machines, configure a domain and HTTPS in System.",
- noAddress: "Core has no public API address yet. Set OAC_PUBLIC_URL to an HTTPS origin.",
+ localOnly: "For access from other machines, set OAC_PUBLIC_URL to an address they can reach.",
+ noAddress: "Core has no public API address yet. Set OAC_PUBLIC_URL.",
model: "Replace {{model}} with a model name your model provider serves, or remove the model field to use this deployment's default model configuration. Running an Agent needs a model provider: pass one in each request, save one on the Agent, or rely on the deployment default. Self-hosted Sessions never use the deployment default.",
keyPlaceholder: "",
projectKey: "Set OPENAI_API_KEY to an API key issued for this project. A key is shown only once, when it is issued; if it's lost, issue a new one.",
diff --git a/apps/web/src/i18n/locales/en/overview.ts b/apps/web/src/i18n/locales/en/overview.ts
index 0ddad3cc..62fcd20e 100644
--- a/apps/web/src/i18n/locales/en/overview.ts
+++ b/apps/web/src/i18n/locales/en/overview.ts
@@ -22,7 +22,7 @@ export const overview = {
},
sandboxes: {
addressFailed: "The installation address could not be read. Retry before confirming sandbox readiness.",
- localOnly: "Configure HTTPS to connect applications and nodes.",
+ localOnly: "Set a public address to connect applications and nodes.",
title: "Get sandboxes ready",
body: "Save where sandboxes run, then connect a node that is online and ready.",
bodyCloud: "Save the E2B account; its template build must be ready.",
diff --git a/apps/web/src/i18n/locales/zh-CN/common.ts b/apps/web/src/i18n/locales/zh-CN/common.ts
index 1148cf18..531ad6e2 100644
--- a/apps/web/src/i18n/locales/zh-CN/common.ts
+++ b/apps/web/src/i18n/locales/zh-CN/common.ts
@@ -10,7 +10,7 @@ export const common = {
},
installationNotice: {
title: "公开地址需要处理",
- body: "连接外部应用和节点前,请先设置一个公网 HTTPS 地址。",
+ body: "连接外部应用和节点前,请先设置一个其他机器能访问的公开地址。",
configure: "查看公开地址",
addBlocked: "公开地址仅限本机访问,暂时无法添加节点。",
},
diff --git a/apps/web/src/i18n/locales/zh-CN/keys.ts b/apps/web/src/i18n/locales/zh-CN/keys.ts
index 40c50861..dd38c590 100644
--- a/apps/web/src/i18n/locales/zh-CN/keys.ts
+++ b/apps/web/src/i18n/locales/zh-CN/keys.ts
@@ -109,8 +109,8 @@ export const keys: TranslationShape = {
copyFailed: "请选中文本后手动复制。",
loading: "正在读取 API 地址",
failed: "无法读取 API 地址。",
- localOnly: "从其他机器调用前,请先把 OAC_PUBLIC_URL 设为一个 HTTPS 源地址。",
- noAddress: "Core 尚未配置公开 API 地址,请把 OAC_PUBLIC_URL 设为一个 HTTPS 源地址。",
+ localOnly: "从其他机器调用前,请先把 OAC_PUBLIC_URL 设为它们能访问的地址。",
+ noAddress: "Core 尚未配置公开 API 地址,请设置 OAC_PUBLIC_URL。",
model: "把 {{model}} 换成模型服务提供的模型名;也可以删掉 model 字段,使用本部署的默认模型配置。运行 Agent 需要模型服务:在每个请求里传入、保存在 Agent 上,或使用部署默认值。自托管 Session 不使用部署默认值。",
keyPlaceholder: "<项目 API key>",
projectKey: "把 OPENAI_API_KEY 设为这个项目签发的 API key。key 只在签发时显示一次;丢失后请签发新 key。",
diff --git a/apps/web/src/i18n/locales/zh-CN/overview.ts b/apps/web/src/i18n/locales/zh-CN/overview.ts
index 06936716..4ecbc010 100644
--- a/apps/web/src/i18n/locales/zh-CN/overview.ts
+++ b/apps/web/src/i18n/locales/zh-CN/overview.ts
@@ -22,7 +22,7 @@ export const overview = {
},
sandboxes: {
addressFailed: "无法读取安装地址,请重试后再确认沙箱是否就绪。",
- localOnly: "配置 HTTPS 后即可连接外部应用和节点。",
+ localOnly: "设置公开地址后即可连接外部应用和节点。",
title: "准备好沙箱",
body: "保存沙箱的运行位置,再接入一台在线且就绪的节点。",
bodyCloud: "保存 E2B 账号,并等它的模板构建就绪。",
diff --git a/apps/web/src/lib/connection.test.ts b/apps/web/src/lib/connection.test.ts
deleted file mode 100644
index 9cc23719..00000000
--- a/apps/web/src/lib/connection.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { isValidDirectCoreBaseUrl } from "./connection";
-
-describe("Core URL checks", () => {
- it.each([
- "https://core.example/v1",
- "http://localhost:8091/v1",
- "http://worker.localhost:8091/v1",
- "http://127.0.0.42:8091/v1",
- "http://2130706433:8091/v1",
- "http://[::1]:8091/v1",
- "http://[0:0:0:0:0:0:0:1]:8091/v1",
- "https://core.example/v1%3Ftenant%3Dsafe",
- "https://core.example/v1%23section",
- "https://core.example/v1/@scope",
- ])("allows HTTPS or an explicit HTTP loopback direct Core: %s", (baseUrl) => {
- expect(isValidDirectCoreBaseUrl(baseUrl)).toBe(true);
- });
-
- it.each([
- "http://core.example/v1",
- "http://192.168.1.20:8091/v1",
- "http://localhost.example/v1",
- "http://127.0.0.1.example/v1",
- "http://127.0.0.1%2eexample/v1",
- "http://[::2]:8091/v1",
- "ftp://core.example/v1",
- "https://user:secret@core.example/v1",
- "https://@core.example/v1",
- "https://:@core.example/v1",
- "https:@core.example/v1",
- "https://core.example/v1?",
- "https://core.example/v1#",
- "https://core.example/v1?#",
- "https://core.example/v1?token=secret",
- "https://core.example/v1#secret",
- ])("rejects an unsafe direct Core URL: %s", (baseUrl) => {
- expect(isValidDirectCoreBaseUrl(baseUrl)).toBe(false);
- });
-});
diff --git a/apps/web/src/lib/connection.ts b/apps/web/src/lib/connection.ts
deleted file mode 100644
index ae61346c..00000000
--- a/apps/web/src/lib/connection.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * URL checks shared by the sandbox pages. The console itself calls only the
- * same-origin Web API; it keeps no Core connection or token in the browser.
- */
-export type CoreConnectionState = "connecting" | "ready" | "failed";
-
-export function isLoopbackHostname(hostname: string): boolean {
- const normalized = hostname.toLowerCase();
- return (
- normalized === "localhost" ||
- normalized.endsWith(".localhost") ||
- normalized === "[::1]" ||
- /^127(?:\.\d{1,3}){3}$/.test(normalized)
- );
-}
-
-function hasExplicitUserInfo(candidate: string): boolean {
- const schemeEnd = candidate.indexOf(":");
- if (schemeEnd < 0) return false;
- const remainder = candidate.slice(schemeEnd + 1).replace(/^[\\/]+/, "");
- const authorityEnd = remainder.search(/[\\/?#]/);
- const authority = authorityEnd < 0 ? remainder : remainder.slice(0, authorityEnd);
- return authority.includes("@");
-}
-
-export function isValidDirectCoreBaseUrl(value: string): boolean {
- try {
- const candidate = value.trim();
- if (candidate.includes("?") || candidate.includes("#") || hasExplicitUserInfo(candidate)) return false;
- const url = new URL(candidate);
- const secureTransport = url.protocol === "https:" || (url.protocol === "http:" && isLoopbackHostname(url.hostname));
- return secureTransport && !url.username && !url.password && !url.search && !url.hash;
- } catch {
- return false;
- }
-}
diff --git a/apps/web/src/lib/locale-strings.ts b/apps/web/src/lib/locale-strings.ts
index b4b76fa5..8e98ba65 100644
--- a/apps/web/src/lib/locale-strings.ts
+++ b/apps/web/src/lib/locale-strings.ts
@@ -163,11 +163,11 @@ export const chinese = {
"Reaches {{core}}, as do its sandboxes": "能访问 {{core}},它的沙箱也要能访问",
"The command creates the oac-node service user and a system service. It installs no software; if something is missing it stops and says what to install.": "命令会创建 oac-node 服务用户和一个系统服务。它不安装任何软件;缺少什么时会停下并说明要装什么。",
"oac-node joins the docker group, which is equivalent to root on this host.": "oac-node 会加入 docker 组,这在这台主机上等同于 root 权限。",
- "Set a public HTTPS address before adding nodes.": "添加节点前,请先设置一个公网 HTTPS 地址。",
+ "Set a public address other machines can reach before adding nodes.": "添加节点前,请先设置其他机器能访问的公开地址。",
"Clean up the host": "清理主机",
"{{name}} is removed from Core. To remove its service and files from the host, run:": "{{name}} 已从 Core 移除。要删除它在主机上的服务和文件,请运行:",
"{{name}} is removed from Core, but its service and files stay on the host.": "{{name}} 已从 Core 移除,但它的服务和文件仍留在主机上。",
- "An uninstall command needs an HTTPS public URL that other machines can reach, and this installation has none.": "卸载命令需要其他机器能访问的 HTTPS 公开地址,而当前安装没有。",
+ "An uninstall command needs a public URL that other machines can reach, and this installation has none.": "卸载命令需要其他机器能访问的公开地址,而当前安装没有。",
"Other machines can't reach this installation's public URL, {{url}}, so no uninstall command can be given.": "其他机器无法访问本安装的公开地址 {{url}},因此无法生成卸载命令。",
"Uninstall command": "卸载命令",
"Copy {{command}}": "复制 {{command}}",
@@ -429,7 +429,7 @@ export const chinese = {
"Config file": "配置文件",
"Then run": "然后运行",
"Copy path": "复制路径",
- "Configure HTTPS in System before connecting remote nodes or E2B sandboxes.": "连接远程节点或 E2B 沙箱前,请先在系统中配置 HTTPS。",
+ "Set a public address before connecting remote nodes; E2B sandboxes need an HTTPS one.": "连接远程节点前,请先设置公开地址;E2B 沙箱需要 HTTPS 地址。",
"Enter the E2B key again to save.": "请重新输入 E2B key 后再保存。",
"Enter the key": "输入 key",
"{{name}} is still bound to an old Core address. Remove it and add it again.": "{{name}} 仍绑定在旧的 Core 地址上,需要移除后重新添加。",
diff --git a/deploy/install.sh b/deploy/install.sh
index 159e4b3b..0836e617 100755
--- a/deploy/install.sh
+++ b/deploy/install.sh
@@ -15,8 +15,10 @@ usage() {
Usage: install.sh [--version TAG] [--install-dir DIR] [--public-url URL]
[--host ADDRESS] [--web-port PORT]
-Installs Core, Web and PostgreSQL, and publishes Web on --web-port. HTTPS is
-terminated by your reverse proxy or hosting platform.
+Installs Core, Web and PostgreSQL, and publishes Web on --web-port. Without
+--public-url, a host published on all addresses with a private-network address
+is reached at http://:; otherwise only from this host.
+HTTPS is terminated by your reverse proxy or hosting platform.
EOF
}
@@ -71,19 +73,41 @@ if [[ "$version" != latest ]]; then
asset_base="https://github.com/${repository}/releases/download/${version}"
fi
+log="$(mktemp)"
cleanup() {
if [[ "$kept" != 1 && -d "$install_dir" ]]; then
(
cd "$install_dir"
+ docker compose logs --no-color --tail 50 >&2 || true
docker compose down --remove-orphans
# Containers own data/; remove it from a container as well.
if [[ -d data ]]; then docker compose run --rm --no-deps --entrypoint find init /data -mindepth 1 -delete; fi
) >/dev/null 2>&1 || true
rm -rf "$install_dir"
fi
+ rm -f "$log"
}
trap cleanup EXIT
+# step DESCRIPTION COMMAND... prints the command's output only when it fails.
+step() {
+ printf '%s... ' "$1"
+ shift
+ if "$@" >"$log" 2>&1; then echo done; else echo failed; cat "$log" >&2; return 1; fi
+}
+
+# The source address of this host's default route, when it is a private one.
+private_address() {
+ ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' |
+ grep -E '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' || true
+}
+local_only=0
+if [[ -z "$public_url" && "$host_address" == 0.0.0.0 ]]; then
+ address="$(private_address)"
+ if [[ -n "$address" ]]; then public_url="http://$address:$web_port"; fi
+fi
+if [[ -z "$public_url" ]]; then public_url="http://localhost:$web_port"; local_only=1; fi
+
mkdir -p "$install_dir"
chmod 700 "$install_dir"
files=(compose.yaml ports.yaml)
@@ -101,24 +125,39 @@ umask 077
echo "OAC_INSTALL_DIR=$install_dir"
echo "OAC_HOST=$host_address"
echo "OAC_WEB_PORT=$web_port"
- if [[ -n "$public_url" ]]; then echo "OAC_PUBLIC_URL=$public_url"; fi
+ echo "OAC_PUBLIC_URL=$public_url"
} >"$install_dir/.env"
-(
- cd "$install_dir"
- docker compose pull
- docker compose create core
- docker compose cp core:/usr/local/bin/oac ./oac
- docker compose up -d --wait
-)
+cd "$install_dir"
+copy_cli() { docker compose create core && docker compose cp core:/usr/local/bin/oac ./oac; }
+step "Pulling images" docker compose pull
+step "Installing the oac command" copy_cli
+step "Starting services" docker compose up -d --wait
+key="$(./oac core-key --show)"
kept=1
trap - EXIT
-address="http://${host_address}:$web_port"
-if [[ -n "$public_url" ]]; then address="$public_url"; fi
-if [[ "$host_address" == 0.0.0.0 || "$host_address" == "::" ]]; then address="http://:$web_port"; fi
+rm -f "$log"
+
+sudo=""
+if [[ "$EUID" == 0 && -n "${SUDO_USER:-}" ]]; then sudo="sudo "; fi
cat <> {log}
if [ "$1" = compose ] && [ "$2" = version ]; then printf 'v2.29.1\\n'; exit 0; fi
- if [ "$1" = compose ] && [ "$2" = cp ]; then printf '#!/bin/sh\\n' > ./oac; exit 0; fi
+ if [ "$1" = compose ] && [ "$2" = cp ]; then printf '#!/bin/sh\\necho oac_core_fixture\\n' > ./oac; chmod +x ./oac; exit 0; fi
if [ "$1" = compose ] && [ "$2" = up ]; then exit {compose_up}; fi
exit 0
"""))
@@ -36,6 +36,7 @@ def install(self, root, *args, compose_up=0):
"""))
self.write_executable(bin_dir / "sha256sum", "#!/bin/sh\nexit 0\n")
self.write_executable(bin_dir / "ss", "#!/bin/sh\nexit 0\n")
+ self.write_executable(bin_dir / "ip", f"#!/bin/sh\nprintf '%s\\n' '{route}'\n")
env = dict(os.environ, PATH=str(bin_dir) + os.pathsep + os.environ["PATH"], HOME=str(root))
completed = subprocess.run(["bash", str(INSTALL), "--install-dir", str(root / "oac"), *args],
env=env, capture_output=True, text=True)
@@ -50,6 +51,27 @@ def test_a_failed_first_start_stops_and_removes_the_directory(self):
self.assertFalse((root / "oac").exists(), "a failed first start must remove the directory")
self.assertIn("compose pull", recorded)
self.assertIn("compose up -d --wait", recorded)
+ self.assertIn("compose logs", recorded, "a failed start must show the services' logs")
+
+ def test_the_private_address_is_the_default_public_url(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ completed, _ = self.install(root)
+ self.assertEqual(completed.returncode, 0, completed.stderr)
+ self.assertIn("OAC_PUBLIC_URL=http://10.0.0.5:8080\n", (root / "oac/.env").read_text())
+ self.assertIn("Console http://10.0.0.5:8080", completed.stdout)
+ self.assertIn("Core key oac_core_fixture", completed.stdout)
+ self.assertNotIn("Only this host", completed.stdout)
+
+ def test_without_a_private_address_only_this_host_reaches_web(self):
+ for args, route in ((["--web-port", "59992"], "1.1.1.1 dev eth0 src 203.0.113.5 uid 0"),
+ (["--host", "127.0.0.1", "--web-port", "59992"], "1.1.1.1 dev eth0 src 10.0.0.5 uid 0")):
+ with self.subTest(args=args), tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ completed, _ = self.install(root, *args, route=route)
+ self.assertEqual(completed.returncode, 0, completed.stderr)
+ self.assertIn("OAC_PUBLIC_URL=http://localhost:59992\n", (root / "oac/.env").read_text())
+ self.assertIn("Only this host can open the console", completed.stdout)
def test_env_holds_only_the_installation_choices(self):
with tempfile.TemporaryDirectory() as temporary:
diff --git a/docs/configuration.md b/docs/configuration.md
index 68fe9c88..bb6bd330 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -29,7 +29,7 @@ Installer flags in [installation options](./getting-started/install-options.md)
### Changing the public URL {#changing-the-public-url}
-`OAC_PUBLIC_URL` is the one origin that applications, nodes, sandboxes and self-hosted executors use. Core derives the daemon WebSocket URL, the self-hosted `remote_url` and each sandbox's connection address from it. The installation serves Web over HTTP on `OAC_WEB_PORT`; your reverse proxy or hosting platform terminates HTTPS and routes to that port.
+`OAC_PUBLIC_URL` is the one origin that applications, nodes, sandboxes and self-hosted executors use. Core derives the daemon WebSocket URL, the self-hosted `remote_url` and each sandbox's connection address from it. It is an http or https origin: the address browsers and nodes use. The installation serves Web over HTTP on `OAC_WEB_PORT`; a reverse proxy or hosting platform terminates HTTPS when you put one in front.
To change it, point the reverse proxy at the new address first, then edit `OAC_PUBLIC_URL` and run `oac apply`. Afterwards:
@@ -43,7 +43,7 @@ To change it, point the reverse proxy at the new address first, then edit `OAC_P
| Variable | Default | Meaning |
| --- | --- | --- |
-| `OAC_PUBLIC_URL` | `http://localhost:8080` | Origin applications, nodes, sandboxes and self-hosted executors use. Managed domain setup writes the HTTPS origin and recreates Core and Web |
+| `OAC_PUBLIC_URL` | `http://localhost:8080` | Origin applications, nodes, sandboxes and self-hosted executors use. See [changing the public URL](#changing-the-public-url) |
| `OAC_HOST` | `127.0.0.1` | Address published by `ports.yaml`. `install.sh` sets `0.0.0.0` |
| `OAC_WEB_PORT` | `8080` | Host port of Web |
| `COMPOSE_FILE` | `compose.yaml:ports.yaml` | The Compose files. `ports.yaml` publishes Web; hosting platforms omit it |
diff --git a/docs/getting-started/install-options.md b/docs/getting-started/install-options.md
index f939a441..a9cd81cd 100644
--- a/docs/getting-started/install-options.md
+++ b/docs/getting-started/install-options.md
@@ -72,7 +72,7 @@ The installer saves no sandbox backend. After signing in, open **System** → **
The default installation publishes Web on `--web-port` (8080) at `--host 0.0.0.0`. Core and PostgreSQL stay private. `--host` is an IPv4 or IPv6 address, without a port, scheme or zone. Use a concrete server IP in the browser, not a wildcard.
-`--public-url` sets `OAC_PUBLIC_URL`, the origin applications, nodes and executors use. Set it to the HTTPS origin your reverse proxy serves.
+`--public-url` sets `OAC_PUBLIC_URL`, the origin applications, nodes and executors use. Behind a reverse proxy, set it to the HTTPS origin the proxy serves. Without it, the installer uses `http://:` when `--host` is `0.0.0.0` and the host's default route has a private-network address, and `http://localhost:` otherwise. [Changing the public URL](../configuration.md#changing-the-public-url) lists the accepted origins.
### Ports
diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md
index 51074514..a4849fd8 100644
--- a/docs/getting-started/install.md
+++ b/docs/getting-started/install.md
@@ -30,7 +30,7 @@ The Core host needs no KVM; nodes that run microsandbox do.
curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash
```
-If a reverse proxy already serves this host, pass its HTTPS address:
+On a host whose default route has a private-network address, the installer sets the public URL to `http://:8080`, so machines on the same network can open Web and add nodes; otherwise only this machine can. If a reverse proxy already serves this host, pass its HTTPS address:
```sh
curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash -s -- --public-url https://core.example
@@ -42,7 +42,7 @@ The script downloads that release's Compose files, checks their SHA-256, and:
2. creates the [installation directory](../configuration.md#installation-directory), `~/.oac/core`, writes `.env`, and copies the `oac` command out of the Core image;
3. starts the services with Docker Compose. Web serves the console on port 8080 and forwards `/v1`, `/api/v1` and `/docs` to Core. Core and PostgreSQL are not published.
-It saves no sandbox backend, adds no node, creates no Project or key and makes no model request. It ends by printing the console address and how to read the Core key.
+It saves no sandbox backend, adds no node, creates no Project or key and makes no model request. It ends by printing the console address and the Core key.
If installation fails before the services become healthy, the installer removes the directory it created. Fix the reported cause and rerun the same command. Once the services have started, a later failure keeps the installation and its data. A new release is a new directory; see [version policy](./operations.md#installation-version-policy).
@@ -50,7 +50,7 @@ For insufficient space or quota, free space on the filesystem named by the error
## Sign in to Web
-1. On this machine, open `http://localhost:8080`, or the origin you passed with `--public-url`. Web accepts only that host.
+1. Open the console address the installer printed, the public URL. Web accepts only that host.
2. Sign in with the [Core key](./operations.md#core-key), the installation's administrator credential. Web has no user accounts.
```sh
@@ -59,7 +59,7 @@ For insufficient space or quota, free space on the filesystem named by the error
## Configure the public address {#configure-the-domain-and-https}
-Applications, nodes and sandboxes reach Core at one HTTPS address, the public URL.
+Applications, nodes and sandboxes reach Core at one address, the public URL. HTTP is enough on the local network. When you expose Core beyond it, put a reverse proxy in front and set the public URL to the HTTPS origin it serves. E2B guests reach Core from the internet, so they need a public URL that is not loopback.
1. Point your reverse proxy at Web.
2. Set `OAC_PUBLIC_URL` to the HTTPS origin it serves, then run `oac apply`. See [changing the public URL](../configuration.md#changing-the-public-url).
diff --git a/docs/getting-started/nodes.md b/docs/getting-started/nodes.md
index 366a8e39..7847678a 100644
--- a/docs/getting-started/nodes.md
+++ b/docs/getting-started/nodes.md
@@ -8,7 +8,7 @@ You add a node by generating a command in Web and running it on the host. The [s
## Before you add a node
-- **Core has an HTTPS public URL** that the host and its sandboxes can reach. Nodes download from Core's console and connect to Core at `public_url`. Until it is set, Add node says *Set a public HTTPS address before adding nodes*; see [Configure the public address](./install.md#configure-the-domain-and-https).
+- **Core has a public URL** that the host and its sandboxes can reach. Nodes download from Core's console and connect to Core at `public_url`. Until it is set, Add node says *Set a public address other machines can reach before adding nodes*; see [Configure the public address](./install.md#configure-the-domain-and-https).
- **The sandbox configuration is saved.** Open **System** → **Manage sandbox configuration**, choose **Own machines**, the backend and a sandbox size, and **Save configuration**. To change a saved configuration, choose **Reset deployment** first. Every node of an installation uses that backend.
- **The console can serve the node files.** Nodes download their Runtime and provider files from the console, which redirects to the release for files it does not hold, and check each file's size and SHA-256 against the release manifest. Node hosts therefore need access to the release. Without the files, Add node says *This console has no node files for …*.
@@ -51,7 +51,7 @@ The installer shows each phase as it runs and, once Core confirms the node, a su
- Docker: rootful Docker Engine running, its socket `/var/run/docker.sock` owned by the `docker` group with mode `0660`, enforcing CPU and memory limits (cgroup v2).
- microsandbox: `/dev/kvm` in the `kvm` group (hardware or nested virtualization), and the libraries microsandbox links (glibc).
- CPUs and memory for at least one sandbox of the installation's size, and about 2 GB of disk for the Runtime image.
-- HTTPS access to the console and Core at the public URL; sandboxes reach Core too.
+- Access to the console and Core at the public URL; sandboxes reach Core too.
### Download through a proxy
diff --git a/docs/getting-started/self-hosted.md b/docs/getting-started/self-hosted.md
index 4dc25ed0..426ab539 100644
--- a/docs/getting-started/self-hosted.md
+++ b/docs/getting-started/self-hosted.md
@@ -20,7 +20,7 @@ The installer brings its own pinned Node.js and Harness versions (listed in [`sc
The machine needs:
-- HTTPS access to Core (plain HTTP only on loopback), and to the release download host unless Core carries an offline copy of the installers;
+- HTTP or HTTPS access to Core, and to the release download host unless Core carries an offline copy of the installers;
- Bash for environment setup and MiniMax Code tools; on Windows, Git Bash, which Claude Code also requires;
- Python and pip when the Session's packages need them;
- any system packages your setup needs. The daemon never runs apt, sudo or another elevation command, so install them through the host's normal administration.
diff --git a/docs/web/console-api-usage.md b/docs/web/console-api-usage.md
index 8c9d0ec3..60195f8b 100644
--- a/docs/web/console-api-usage.md
+++ b/docs/web/console-api-usage.md
@@ -75,7 +75,7 @@ In an archived project the section hides **Issue credential** and **Rotate** beh
| Installation | `GET /core/v1/installation` | System's Installation facts (`public_url`, `api_base_url`, `installation_id`, `source_commit`) and read-only Startup settings (`configuration.settings` under its `path`, `apply_command` and `applied_at`; a sensitive setting shows only whether it is `configured`); `api_base_url` in the call samples; `public_url` as the download origin and `--source-url` of the node install and uninstall commands (and the install command's `--core-url`); `path` and `apply_command` beside a sandbox configuration Core rejected. A sensitive setting with a value, or an unknown member, fails the read; `configuration: null` shows a note |
| Core metrics | `GET /core/v1/metrics?range=` | Core metrics page; the Core popover on Overview. A Core without the route (404) is shown as not reporting, and the popover then shows only Core's status. The [Core metrics contract](../../contracts/agents-api/core-metrics.md) defines every measurement |
-`local_only`, or a `public_url` that is not an HTTPS origin, stops Add node from issuing a command and Clean up the host from giving one. Overview, Nodes and System then show a visible warning with Core's configuration path and apply command as copyable values; when `configuration` is null, they state that the path and command are unavailable. Nodes disables Add node with a visible reason, and Getting started leaves its sandbox step to do.
+`local_only`, or no `public_url`, stops Add node from issuing a command and Clean up the host from giving one. Overview, Nodes and System then show a visible warning with Core's configuration path and apply command as copyable values; when `configuration` is null, they state that the path and command are unavailable. Nodes disables Add node with a visible reason, and Getting started leaves its sandbox step to do.
Wherever a new key is shown, and without any key on an active project's page, the console gives shell exports of `OPENAI_BASE_URL` (the installation's `api_base_url`) and `OPENAI_API_KEY` (the new key, or a placeholder for a key of the project), with `curl` and Python examples for `GET /v1/agents` and `POST /v1/agents/sessions`, and sends none of them. When the installation is `local_only` it says the API is reachable only on the Core machine, and without an `api_base_url` it says to set `public_url`.
diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md
index fb742b5d..f713c20b 100644
--- a/docs/zh/configuration.md
+++ b/docs/zh/configuration.md
@@ -1,7 +1,7 @@
---
title: "配置参考"
source: docs/configuration.md
-source_hash: 62ad7f6c64329e0d3e9bda9b4936f5c13281eb18a1011b057b2f6288ddad35cc
+source_hash: 89ce54ec713de69bbc2d8aaa89ddb577744d0ca694c3fdbca8bf2a452200ad46
---
Core 安装的每项设置都恰好只有一个归属位置。共有两类:
@@ -31,7 +31,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置
### 更改公共 URL {#changing-the-public-url}
-`OAC_PUBLIC_URL` 是应用、节点、沙箱和自托管执行器使用的唯一源地址。Core 从中派生守护进程 WebSocket URL、自托管 `remote_url` 和每个沙箱的连接地址。安装通过 `OAC_WEB_PORT` 以 HTTP 提供 Web;反向代理或托管平台终止 HTTPS 并把流量转到该端口。
+`OAC_PUBLIC_URL` 是应用、节点、沙箱和自托管执行器使用的唯一源地址。Core 从中派生守护进程 WebSocket URL、自托管 `remote_url` 和每个沙箱的连接地址。它是 http 或 https 源地址,也就是浏览器和节点使用的地址。安装通过 `OAC_WEB_PORT` 以 HTTP 提供 Web;前面有反向代理或托管平台时,由它们终止 HTTPS。
要更改它,先把反向代理指向新地址,然后编辑 `OAC_PUBLIC_URL` 并运行 `oac apply`。之后:
@@ -47,7 +47,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置
| Variable | Default | Meaning |
| --- | --- | --- |
-| `OAC_PUBLIC_URL` | `http://localhost:8080` | Origin applications, nodes, sandboxes and self-hosted executors use. Managed domain setup writes the HTTPS origin and recreates Core and Web |
+| `OAC_PUBLIC_URL` | `http://localhost:8080` | 应用、节点、沙箱和自托管执行器使用的源地址。参阅[修改公开 URL](#changing-the-public-url) |
| `OAC_HOST` | `127.0.0.1` | Address published by `ports.yaml`. `install.sh` sets `0.0.0.0` |
| `OAC_WEB_PORT` | `8080` | Host port of Web |
| `COMPOSE_FILE` | `compose.yaml:ports.yaml` | Compose 文件。`ports.yaml` 发布 Web;托管平台省略它 |
diff --git a/docs/zh/getting-started/install-options.md b/docs/zh/getting-started/install-options.md
index 11bd0df5..d578ae6f 100644
--- a/docs/zh/getting-started/install-options.md
+++ b/docs/zh/getting-started/install-options.md
@@ -1,7 +1,7 @@
---
title: "安装选项与高级部署"
source: docs/getting-started/install-options.md
-source_hash: be5e1a127f654e75611c7b670fba9fc10cb3b78165590a7f6616eb1b79dd2f14
+source_hash: 4b847976f3b3bc4fe4afe7b7946c5815df9fecf75dc39648621814e793288f9e
---
[默认安装](install.md)无需任何选项。使用本页可以在现有反向代理后运行,或者在无法访问互联网时进行安装。
@@ -76,7 +76,7 @@ docker compose -f compose.yaml exec web oac-web core-key
默认安装在 `--host 0.0.0.0` 的 `--web-port`(8080)上发布 Web。Core 和 PostgreSQL 保持私有。`--host` 是不含端口、协议或区域的 IPv4 或 IPv6 地址。请在浏览器中使用服务器的具体 IP,而不是通配地址。
-`--public-url` 设置 `OAC_PUBLIC_URL`,即应用、节点和执行器使用的源地址。把它设为反向代理提供的 HTTPS 源地址。
+`--public-url` 设置 `OAC_PUBLIC_URL`,即应用、节点和执行器使用的源地址。使用反向代理时,把它设为代理提供的 HTTPS 源地址。未传入时,如果 `--host` 为 `0.0.0.0` 且主机默认路由的源地址是私有网络地址,安装程序使用 `http://:`,否则使用 `http://localhost:`。可接受的源地址见[修改公开 URL](../configuration.md#changing-the-public-url)。
### 端口 {#ports}
diff --git a/docs/zh/getting-started/install.md b/docs/zh/getting-started/install.md
index 7f4c148a..55e711e8 100644
--- a/docs/zh/getting-started/install.md
+++ b/docs/zh/getting-started/install.md
@@ -1,7 +1,7 @@
---
title: "安装 Core 和 Web"
source: docs/getting-started/install.md
-source_hash: 63db48e9ac2b0f64b773b7271511c95dc44121ff29f303a738f124ec8c76c8a3
+source_hash: b588dc9d68ba909fe9fadc440ee3f6fda9b79ebe0f14f6011d8bab603e17c710
---
一条命令即可在 Linux 主机上安装 Core、Web 控制台和 PostgreSQL。用 Core 密钥登录 Web,设置默认模型并签发 Project API 密钥。应用使用这些密钥调用 Core。Session 在你添加的节点上的沙箱中运行,也可以在 E2B 上运行。
@@ -32,7 +32,7 @@ Core 主机不需要 KVM;运行 microsandbox 的节点需要。
curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash
```
-反向代理已经提供这台主机时,传入它的 HTTPS 地址:
+如果主机默认路由的源地址是私有网络地址,安装程序把公开 URL 设为 `http://:8080`,同一网络的机器即可打开 Web 并添加节点;否则只有本机可以访问。反向代理已经提供这台主机时,传入它的 HTTPS 地址:
```sh
curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install.sh | bash -s -- --public-url https://core.example
@@ -44,7 +44,7 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/
2. 创建[安装目录](../configuration.md#installation-directory) `~/.oac/core`,写入 `.env`,并从 Core 镜像复制 `oac` 命令;
3. 用 Docker Compose 启动服务。Web 在 8080 端口提供控制台,并把 `/v1`、`/api/v1` 和 `/docs` 转发到 Core。Core 和 PostgreSQL 不发布端口。
-安装程序不保存沙箱后端,不添加节点,不创建 Project 或密钥,也不发起模型请求。完成后输出控制台地址,以及如何读取 Core 密钥。
+安装程序不保存沙箱后端,不添加节点,不创建 Project 或密钥,也不发起模型请求。完成后输出控制台地址和 Core 密钥。
如果服务进入健康状态之前安装失败,安装程序会删除它创建的目录。修复报告的问题后,重新运行同一命令。服务已经启动之后,后续失败会保留安装和数据。新发布版使用新目录;见[版本策略](operations.md#installation-version-policy)。
@@ -52,7 +52,7 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/
## 登录 Web {#sign-in-to-web}
-1. 在本机打开 `http://localhost:8080`,或打开 `--public-url` 传入的源地址。Web 只接受这个主机名。
+1. 打开安装程序输出的控制台地址,即公开 URL。Web 只接受这个主机名。
2. 使用 [Core 密钥](operations.md#core-key)登录,这是安装的管理员凭据。Web 没有用户账号。
```sh
@@ -61,7 +61,7 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/
## 配置公开地址 {#configure-the-domain-and-https}
-应用、节点和沙箱通过同一个 HTTPS 地址访问 Core,即公开 URL。
+应用、节点和沙箱通过同一个地址访问 Core,即公开 URL。局域网上用 HTTP 即可。对外暴露时,在前面放反向代理,并把公开 URL 设为它提供的 HTTPS 源地址。E2B 客户机从互联网访问 Core,因此需要非回环的公开 URL。
1. 把反向代理指向 Web。
2. 把 `OAC_PUBLIC_URL` 设为反向代理提供的 HTTPS 源地址,然后运行 `oac apply`。见[修改公开 URL](../configuration.md#changing-the-public-url)。
diff --git a/docs/zh/getting-started/nodes.md b/docs/zh/getting-started/nodes.md
index ab996dca..c0097dc8 100644
--- a/docs/zh/getting-started/nodes.md
+++ b/docs/zh/getting-started/nodes.md
@@ -1,7 +1,7 @@
---
title: "添加和管理节点"
source: docs/getting-started/nodes.md
-source_hash: f7bfd1ac05cbed8a1f31badee219164c5e1a16aa897e89551abeb8b8d0010215
+source_hash: 863ce05f8cfbc5da7c9f05c35c361ebd15a99fc3cebc952e2c8076810ff3fe3b
---
节点是一台 Linux 主机,在沙箱后端为 Docker 或 microsandbox 时,为 Core 托管 Session 运行沙箱。Core 将新 Session 分配给有空余容量的节点;节点创建沙箱,沙箱回连 Core。E2B 不需要节点。应用为自己的 Session 连接的机器是[自托管执行器](self-hosted.md),而不是节点。
@@ -10,7 +10,7 @@ source_hash: f7bfd1ac05cbed8a1f31badee219164c5e1a16aa897e89551abeb8b8d0010215
## 添加节点前 {#before-you-add-a-node}
-- **Core 已有主机及沙箱可访问的 HTTPS 公开 URL。** 节点从 Core 控制台下载文件,并通过 `public_url` 连接 Core。设置前,Add node 显示 *Set a public HTTPS address before adding nodes*;参阅[配置公开地址](install.md#configure-the-domain-and-https)。
+- **Core 已有主机及沙箱可访问的公开 URL。** 节点从 Core 控制台下载文件,并通过 `public_url` 连接 Core。设置前,Add node 显示 *Set a public address other machines can reach before adding nodes*;参阅[配置公开地址](install.md#configure-the-domain-and-https)。
- **沙箱配置已保存。** 打开 **System** → **Manage sandbox configuration**,选择 **Own machines**、后端和沙箱规格,最后选择 **Save configuration**。要更改已保存的配置,先选择 **Reset deployment**。同一安装的所有节点使用同一后端。
- **控制台能提供节点文件。** 节点从控制台下载 Runtime 和提供商文件;控制台缺少文件时重定向到发行下载地址。节点依据发行清单检查各文件的大小和 SHA-256。因此节点主机需要能访问发行下载地址。缺少文件时,Add node 显示 *This console has no node files for …*。
@@ -53,7 +53,7 @@ printf '%s\n' '' | $s python3 "$d/node-install.pyz" ${NO_COLOR
- Docker:正在运行的 rootful Docker Engine,其 `/var/run/docker.sock` 套接字属于 `docker` 组,权限为 `0660`,并强制执行 CPU 和内存限制(cgroup v2)。
- microsandbox:`/dev/kvm` 属于 `kvm` 组(硬件或嵌套虚拟化),并具有 microsandbox 链接的库(glibc)。
- CPU 和内存至少足以运行一个所配置规格的沙箱,以及约 2 GB 的 Runtime 镜像磁盘空间。
-- 可通过公开 URL 以 HTTPS 访问控制台和 Core;沙箱也能访问 Core。
+- 可通过公开 URL 访问控制台和 Core;沙箱也能访问 Core。
### 通过代理下载 {#download-through-a-proxy}
diff --git a/docs/zh/getting-started/self-hosted.md b/docs/zh/getting-started/self-hosted.md
index 524f13b8..7765a173 100644
--- a/docs/zh/getting-started/self-hosted.md
+++ b/docs/zh/getting-started/self-hosted.md
@@ -1,7 +1,7 @@
---
title: "自托管执行器"
source: docs/getting-started/self-hosted.md
-source_hash: c560a575e51c02ccb474a629f5655e59779643696826c7677d0b5a949be2e5fc
+source_hash: 800b3eb891c20d34215344c0ea147dbb79b5921d25d6f2405bcfe5edccd88038
---
`self_hosted` Session 在应用拥有的机器上运行:工作站、虚拟机或你管理的沙箱。应用通过 `/v1` 创建 Session,并获得安装 `oac-daemon`、启动它并连接 Core 的命令。Web 在 Session 页面展示同一命令;Web 是可选的。Core 不创建、停止或回收这台机器。
@@ -22,7 +22,7 @@ Session 自带模型提供商;安装默认模型不适用([原因](../../../
机器需要:
-- 通过 HTTPS 访问 Core(仅回环地址允许明文 HTTP),以及访问发布下载主机;如果 Core 已有安装程序的离线副本,则无需后者;
+- 通过 HTTP 或 HTTPS 访问 Core,以及访问发布下载主机;如果 Core 已有安装程序的离线副本,则无需后者;
- 用于环境设置和 MiniMax Code 工具的 Bash;Windows 上需要 Git Bash,Claude Code 也要求它;
- Session 的软件包需要时,安装 Python 和 pip;
- 环境设置所需的系统软件包。守护进程不运行 apt、sudo 或其他提权命令,请通过主机的常规管理方式安装。
diff --git a/docs/zh/web/console-api-usage.md b/docs/zh/web/console-api-usage.md
index 42d3bd58..09d91aff 100644
--- a/docs/zh/web/console-api-usage.md
+++ b/docs/zh/web/console-api-usage.md
@@ -1,7 +1,7 @@
---
title: "控制台 API 使用"
source: docs/web/console-api-usage.md
-source_hash: 4e3bfbad317f6ab1467de7716a9463c088f12a2f88f84f36b08d37be64dc3db6
+source_hash: 2a4be7b081286e5a95c14380acc517d2d4b4ba955c0bd61338b0977d84c9df16
---
本页列出各控制台页面读取和写入的 Core 路由,以及控制台如何限定读取范围。[administrator API contract](../../../contracts/agents-api/zh/admin-api.md) 定义了路由、响应结构、分页和审计记录;[API namespaces and credentials](../api/index.md) 定义了本文使用的术语。
@@ -77,7 +77,7 @@ source_hash: 4e3bfbad317f6ab1467de7716a9463c088f12a2f88f84f36b08d37be64dc3db6
| 安装 | `GET /core/v1/installation` | System 的 Installation 信息(`public_url`、`api_base_url`、`installation_id`、`source_commit`)和只读 Startup 设置(`path` 下的 `configuration.settings`,以及 `apply_command` 和 `applied_at`;敏感设置仅显示其是否为 `configured`);调用示例中的 `api_base_url`;作为下载来源以及节点安装和卸载命令中 `--source-url` 的 `public_url`(还包括安装命令中的 `--core-url`);Core 拒绝的 Sandbox 配置旁的 `path` 和 `apply_command`。如果敏感设置包含值,或存在未知成员,读取会失败;`configuration: null` 会显示一条说明 |
| Core 指标 | `GET /core/v1/metrics?range=` | Core 指标页面;Overview 上的 Core 弹出内容。不存在该路由的 Core(404)会显示为未报告数据,此时弹出内容仅显示 Core 状态。[Core metrics contract](../../../contracts/agents-api/zh/core-metrics.md) 定义了每项度量 |
-如果为 `local_only`,或者 `public_url` 不是 HTTPS 来源,Add node 将无法签发命令,Clean up the host 也无法提供命令。随后 Overview、Nodes 和 System 会显示醒目警告,其中 Core 的配置路径和 apply command 为可复制值;当 `configuration` 为 null 时,它们会说明路径和命令不可用。Nodes 会禁用 Add node 并显示明确原因,Getting started 则将 sandbox 步骤保留为待办项。
+如果为 `local_only`,或者没有 `public_url`,Add node 将无法签发命令,Clean up the host 也无法提供命令。随后 Overview、Nodes 和 System 会显示醒目警告,其中 Core 的配置路径和 apply command 为可复制值;当 `configuration` 为 null 时,它们会说明路径和命令不可用。Nodes 会禁用 Add node 并显示明确原因,Getting started 则将 sandbox 步骤保留为待办项。
无论是在显示新密钥时,还是在活动项目页面没有显示任何密钥时,控制台都会提供 `OPENAI_BASE_URL`(安装的 `api_base_url`)和 `OPENAI_API_KEY`(新密钥,或项目密钥的占位符)的 shell 导出变量,以及针对 `GET /v1/agents` 和 `POST /v1/agents/sessions` 的 `curl` 和 Python 示例,但不会发送其中任何调用。当安装为 `local_only` 时,控制台会说明 API 只能在 Core 所在计算机上访问;当缺少 `api_base_url` 时,则会提示设置 `public_url`。
diff --git a/services/core/cmd/sandbox-node/main.go b/services/core/cmd/sandbox-node/main.go
index 68749e61..11657f15 100644
--- a/services/core/cmd/sandbox-node/main.go
+++ b/services/core/cmd/sandbox-node/main.go
@@ -53,7 +53,7 @@ func run(ctx context.Context, args []string) error {
flags := flag.NewFlagSet("sandbox-node "+args[0], flag.ContinueOnError)
configFile := flags.String("config", "", "absolute provider configuration file")
stateDir := flags.String("state-dir", "", "absolute private node state directory")
- coreURL := flags.String("core-url", "", "Core HTTPS origin (register only)")
+ coreURL := flags.String("core-url", "", "Core origin (register only)")
name := flags.String("name", "sandbox-node", "display name (register only)")
tokenFile := flags.String("enrollment-token-file", "", "private single-use enrollment token file (register only)")
if err := flags.Parse(args[1:]); err != nil {
diff --git a/services/core/cmd/server/process_configuration_test.go b/services/core/cmd/server/process_configuration_test.go
index a735035c..eef02edc 100644
--- a/services/core/cmd/server/process_configuration_test.go
+++ b/services/core/cmd/server/process_configuration_test.go
@@ -35,13 +35,13 @@ func TestExecutionConcurrencyConfiguration(t *testing.T) {
}
func TestPublicURLMustBeACanonicalOrigin(t *testing.T) {
- for _, value := range []string{"https://core.example", "https://core.example:8443", "http://127.0.0.1:8091"} {
+ for _, value := range []string{"https://core.example", "https://core.example:8443", "http://127.0.0.1:8091", "http://core.example"} {
t.Setenv("OAC_PUBLIC_URL", value)
if got, err := processconfig.PublicURL(); err != nil || got != value {
t.Fatal(value, got, err)
}
}
- for _, value := range []string{"https://core.example/", "https://Core.example", "http://core.example", "wss://core.example", "https://core.example/v1"} {
+ for _, value := range []string{"https://core.example/", "https://Core.example", "wss://core.example", "https://core.example/v1"} {
t.Setenv("OAC_PUBLIC_URL", value)
if _, err := processconfig.PublicURL(); err == nil {
t.Fatal("accepted", value)
diff --git a/services/core/internal/deployment/public_url.go b/services/core/internal/deployment/public_url.go
index a676955f..999bcd55 100644
--- a/services/core/internal/deployment/public_url.go
+++ b/services/core/internal/deployment/public_url.go
@@ -9,7 +9,8 @@ import (
)
// ValidateCoreURL accepts a canonical public origin, never a path or
-// credential. Plain HTTP is reserved for explicit loopback development hosts.
+// credential. It may be http or https: a reverse proxy in front of Web
+// terminates TLS when the installation uses it.
// OAC_PUBLIC_URL must pass it.
func ValidateCoreURL(value string) error {
u, err := url.Parse(value)
@@ -25,10 +26,7 @@ func ValidateCoreURL(value string) error {
return ErrInvalidInput
}
}
- loopback := u.Hostname() == "localhost"
- if ip := net.ParseIP(u.Hostname()); ip != nil {
- loopback = ip.IsLoopback()
- } else {
+ if net.ParseIP(u.Hostname()) == nil {
if len(u.Hostname()) > 253 || strings.ContainsAny(u.Host, "[]") {
return ErrInvalidInput
}
@@ -43,7 +41,7 @@ func ValidateCoreURL(value string) error {
}
}
}
- if u.Scheme != "https" && !(u.Scheme == "http" && loopback) {
+ if u.Scheme != "https" && u.Scheme != "http" {
return ErrInvalidInput
}
return nil
diff --git a/services/core/internal/deployment/rules_test.go b/services/core/internal/deployment/rules_test.go
index 196e4586..66fd2af3 100644
--- a/services/core/internal/deployment/rules_test.go
+++ b/services/core/internal/deployment/rules_test.go
@@ -14,13 +14,13 @@ import (
)
func TestValidateCoreURL(t *testing.T) {
- for _, value := range []string{"https://core.example", "https://core.example:8443", "http://localhost:8091", "http://127.0.0.2:8091", "http://[::1]:8091", "https://[2001:db8::1]"} {
+ for _, value := range []string{"https://core.example", "https://core.example:8443", "http://localhost:8091", "http://127.0.0.2:8091", "http://[::1]:8091", "https://[2001:db8::1]", "http://core.example", "http://core:8091", "http://10.0.0.5:8080"} {
if err := ValidateCoreURL(value); err != nil {
t.Errorf("valid Core URL %q rejected: %v", value, err)
}
}
for _, value := range []string{
- "", "http://core.example", "http://core:8091", "http://host.localhost", "https://core.example/", "https://user:secret@core.example",
+ "", "ftp://core.example", "ws://core.example", "https://core.example/", "https://user:secret@core.example",
"https://core.example/path", "https://core.example?", "https://core.example?q=x", "https://core.example#x", "https://core.example#",
"https://CORE.example", "https://core.example:", "https://core.example:0", "https://core.example:65536", "https://core.example:0080",
"https://core.example\\evil", "https://[not-an-ip]", "https://-core.example", "https://core..example", "https://core_example",
diff --git a/services/core/internal/processconfig/config.go b/services/core/internal/processconfig/config.go
index d846819e..c7f1805b 100644
--- a/services/core/internal/processconfig/config.go
+++ b/services/core/internal/processconfig/config.go
@@ -107,7 +107,7 @@ func PublicURL() (string, error) {
return "", nil
}
if deployment.ValidateCoreURL(value) != nil {
- return "", configErr("OAC_PUBLIC_URL must be a canonical HTTPS origin without path, credentials, query or fragment, such as https://core.example; plain HTTP is accepted only for a loopback host")
+ return "", configErr("OAC_PUBLIC_URL must be a canonical http or https origin without path, credentials, query or fragment, such as https://core.example")
}
return value, nil
}
diff --git a/services/core/internal/sandbox/node/identity.go b/services/core/internal/sandbox/node/identity.go
index 79567666..ec24276a 100644
--- a/services/core/internal/sandbox/node/identity.go
+++ b/services/core/internal/sandbox/node/identity.go
@@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"io"
- "net"
"net/url"
"os"
"path/filepath"
@@ -155,11 +154,8 @@ func endpoint(raw, path string) (string, error) {
if err != nil || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") {
return "", errors.New("node Core URL must be an origin")
}
- if u.Scheme != "https" {
- ip := net.ParseIP(u.Hostname())
- if u.Scheme != "http" || !(u.Hostname() == "localhost" || ip != nil && ip.IsLoopback()) {
- return "", errors.New("remote node Core URL requires HTTPS")
- }
+ if u.Scheme != "https" && u.Scheme != "http" {
+ return "", errors.New("node Core URL must use http or https")
}
u.Path = path
return u.String(), nil
diff --git a/services/core/internal/sandbox/node/node_test.go b/services/core/internal/sandbox/node/node_test.go
index 2d375be1..977d00c8 100644
--- a/services/core/internal/sandbox/node/node_test.go
+++ b/services/core/internal/sandbox/node/node_test.go
@@ -283,13 +283,13 @@ func TestEnrollmentLostResponseRecoversWithPersistedCredential(t *testing.T) {
}
}
-func TestCoreURLRejectsRemotePlaintextAndCredentials(t *testing.T) {
- for _, raw := range []string{"http://example.com", "https://user:pass@example.com", "https://example.com/?token=x", "https://example.com/path"} {
+func TestCoreURLRejectsOtherSchemesAndCredentials(t *testing.T) {
+ for _, raw := range []string{"ftp://example.com", "https://user:pass@example.com", "https://example.com/?token=x", "https://example.com/path"} {
if _, err := endpoint(raw, "/api/v1/sandbox-node/enroll"); err == nil {
t.Fatalf("accepted %q", raw)
}
}
- for _, raw := range []string{"https://core.example.test:9443", "http://127.0.0.1:8080", "http://[::1]:8080"} {
+ for _, raw := range []string{"https://core.example.test:9443", "http://127.0.0.1:8080", "http://[::1]:8080", "http://core.example.test:8080"} {
if _, err := endpoint(raw, "/api/v1/sandbox-node/enroll"); err != nil {
t.Fatalf("rejected %q: %v", raw, err)
}