Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions apps/daemon/internal/cli/connect_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions apps/daemon/internal/cli/connect_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 3 additions & 3 deletions apps/web/e2e/overview-readiness.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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]) {
Expand Down Expand Up @@ -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();
});
2 changes: 1 addition & 1 deletion apps/web/e2e/public-url.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/InstallationNotice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe("local-only installation notice", () => {
it("links to the public address without exposing installer commands", () => {
const html = renderToStaticMarkup(<InstallationNotice installation={installation} />);
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");
});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/sandbox/NodeCleanupDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function NodeCleanupDialog({ cleanup, open, onClose }: { cleanup: NodeCle
</div> : cleanup && !sourceUrl ? <div className="sandbox-add-node form-stack">
<p>{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."))}</p>
: t("An uninstall command needs a public URL that other machines can reach, and this installation has none."))}</p>
</div> : cleanup ? <div className="sandbox-add-node form-stack">
<p>{t("{{name}} is removed from Core. To remove its service and files from the host, run:", { name: cleanup.name })}</p>
<CommandBlock key={command()} value={command()} label={t("Uninstall command")} autoFocus />
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sandbox/NodeEnrollment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/sandbox/SandboxSetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
<dd>
{address ? <code>{address}</code> : "—"}
<span className="wizard-review-sub">{t("Managed in System")}</span>
{installation.data?.local_only ? <span className="wizard-review-caution">{t("Configure HTTPS in System before connecting remote nodes or E2B sandboxes.")}</span> : null}
{installation.data?.local_only ? <span className="wizard-review-caution">{t("Set a public address before connecting remote nodes; E2B sandboxes need an HTTPS one.")}</span> : null}
</dd>
</div>
</dl>
Expand Down
17 changes: 4 additions & 13 deletions apps/web/src/features/sandbox/core-origin.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
21 changes: 4 additions & 17 deletions apps/web/src/features/sandbox/core-origin.ts
Original file line number Diff line number Diff line change
@@ -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<CoreInstallation, "public_url" | "local_only">): 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;
}
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/en/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/i18n/locales/en/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<project API key>",
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.",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/en/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/zh-CN/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const common = {
},
installationNotice: {
title: "公开地址需要处理",
body: "连接外部应用和节点前,请先设置一个公网 HTTPS 地址。",
body: "连接外部应用和节点前,请先设置一个其他机器能访问的公开地址。",
configure: "查看公开地址",
addBlocked: "公开地址仅限本机访问,暂时无法添加节点。",
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/i18n/locales/zh-CN/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ export const keys: TranslationShape<typeof english> = {
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。",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/zh-CN/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const overview = {
},
sandboxes: {
addressFailed: "无法读取安装地址,请重试后再确认沙箱是否就绪。",
localOnly: "配置 HTTPS 后即可连接外部应用和节点。",
localOnly: "设置公开地址后即可连接外部应用和节点。",
title: "准备好沙箱",
body: "保存沙箱的运行位置,再接入一台在线且就绪的节点。",
bodyCloud: "保存 E2B 账号,并等它的模板构建就绪。",
Expand Down
41 changes: 0 additions & 41 deletions apps/web/src/lib/connection.test.ts

This file was deleted.

36 changes: 0 additions & 36 deletions apps/web/src/lib/connection.ts

This file was deleted.

Loading
Loading