From df78507cab67c707d034638ca83ee0e301728176 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Wed, 7 Oct 2026 17:29:55 -0700 Subject: [PATCH 1/5] Route registry traffic to relay-agent Session-Id: 01a1176f-b97d-7942-a94a-61f5b8b38013 --- router/README.md | 3 ++ router/index.ts | 54 +++++++++++++++++++++++++----- router/test/agent-chat.test.ts | 60 ++++++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/router/README.md b/router/README.md index 68963dbb..2798fe20 100644 --- a/router/README.md +++ b/router/README.md @@ -5,6 +5,9 @@ Route precedence: - `GET`/`HEAD /connect/` (Relay Connect invite links) is served by the Cloud app's `/cloud/connect/` route. - `/observer/file*` routes to the RelayFile observer app, expected to be built with `basePath: "/observer/file"`. - `/observer*` routes to the Relaycast observer app. +- On `arelay.to`, `POST //<32hex>`, `GET /register`, and + `/api/v1/(registrations|agents)/**` route to the `relay-agent` Worker. The + Worker owns registry lookup and its 404/410 decisions. - everything else falls back to the legacy proxy target. RelayFile observer deployment is wired from the production job in diff --git a/router/index.ts b/router/index.ts index f6a67eaf..360c563b 100644 --- a/router/index.ts +++ b/router/index.ts @@ -43,10 +43,11 @@ const CONNECT_INVITE_PATH = /^(\/connect\/[A-Za-z0-9_-]{32,64}(?:\.(?:json|md))? // site, but its bare root and the signed-in cloud app send people to agentrelay.com. const SHORT_HOST = "arelay.to"; const SHORT_HOST_WWW = "www.arelay.to"; -// Agents a visitor's coding agent can chat with from a pasted snippet. The -// conversation endpoint is //<32 hex id> on either host. -const AGENT_CHAT_AGENTS = new Set(["agent-relay"]); +// A visitor's coding agent chats through //<32 hex id>. arelay.to +// accepts every handle-shaped slug and lets relay-agent's registry decide +// whether it exists. agentrelay.com retains its established agent-relay route. const AGENT_CHAT_PATH = /^\/([a-z0-9-]+)\/([0-9a-f]{32})\/?$/; +const REGISTRY_API_PATH = /^\/api\/v1\/(?:registrations|agents)(?:\/|$)/; const WEBHOOK_ORIGIN_FLAG_KEY = "WEBHOOK_ORIGIN"; export const WILL_CALENDAR_URL = "https://calendar.app.google/RqLuQyT3dYe5e2YdA"; export const KHALIQ_CALENDAR_URL = "https://calendly.com/khaliq-agent-relay/30min"; @@ -135,7 +136,7 @@ export function getAgentChatCloudPath( } const match = AGENT_CHAT_PATH.exec(pathname); - if (!match || !AGENT_CHAT_AGENTS.has(match[1])) { + if (!match || (hostname === PRIMARY_HOST && match[1] !== "agent-relay")) { return undefined; } return `${CLOUD_PATH_PREFIX}/api/v1/agent-chat/${match[1]}/${match[2]}`; @@ -162,7 +163,7 @@ export function getAgentPagePath( } else if (hostname === PRIMARY_HOST) { agent = /^\/u\/([a-z0-9-]+)\/?$/.exec(pathname)?.[1]; } - if (!agent || !AGENT_CHAT_AGENTS.has(agent)) { + if (!agent) { return undefined; } const wantsHtml = method === "HEAD" || prefersHtmlOverMarkdown(accept); @@ -216,9 +217,23 @@ export function getLegacyAgentPageRedirect(url: URL): string | undefined { return undefined; } const agent = /^\/([a-z0-9-]+)\/?$/.exec(url.pathname)?.[1]; - return agent && AGENT_CHAT_AGENTS.has(agent) + return agent === "agent-relay" ? `https://${PRIMARY_HOST}/u/${agent}${url.search}` : undefined; + +// Registration and management are served directly by relay-agent. The public +// API matcher is prefix-bounded so lookalikes such as /registrations-legacy do +// not escape the marketing site. +export function isRelayAgentRegistryRoute( + hostname: string, + pathname: string, + method: string, +): boolean { + if (hostname !== SHORT_HOST) return false; + if ((pathname === "/register" || pathname === "/register/") && method === "GET") { + return true; + } + return REGISTRY_API_PATH.test(pathname); } // arelay.to only fronts agent chat: its root, its www alias and the signed-in @@ -554,7 +569,13 @@ export default { request.method, request.headers.get("accept"), ); - const shortHostRedirect = agentChatCloudPath || agentPagePath + const registryRoute = isRelayAgentRegistryRoute( + url.hostname, + url.pathname, + request.method, + ); + const relayAgentRoute = Boolean(agentChatCloudPath || registryRoute); + const shortHostRedirect = relayAgentRoute || agentPagePath ? undefined : getShortHostRedirect(url); if (shortHostRedirect) { @@ -592,13 +613,13 @@ export default { // content, so neither the new route nor the existing Cloud fallback belongs // in the replay corpus. Agent pages and guides mint a conversation URL in // every response, so they stay out too, however they were reached. - const recorderRequestClone = recorderEnv && !agentChatCloudPath && !isAgentPageRequestPath(url.pathname) + const recorderRequestClone = recorderEnv && !relayAgentRoute && !isAgentPageRequestPath(url.pathname) ? (request.clone() as unknown as Request) : null; // Production config includes the service binding. Removing it (and any // origin alternative) is the rollback flag that restores the Cloud route. - if (agentChatCloudPath && relayAgentEnabled(env)) { + if (relayAgentRoute && relayAgentEnabled(env)) { try { const workerResponse = await fetchRelayAgent(request, url, env); return workerResponse; @@ -621,6 +642,21 @@ export default { } } + // Unlike the legacy agent-relay chat route, registry endpoints have no + // Cloud fallback. Keep them off the marketing origin when the dedicated + // Worker is absent during a rollback or unavailable environment. + if (registryRoute) { + return new Response("The agent registry is unavailable. Retry shortly.\n", { + status: 503, + headers: { + "content-type": "text/plain; charset=utf-8", + "cache-control": "no-store", + "referrer-policy": "no-referrer", + "x-content-type-options": "nosniff", + }, + }); + } + const connectInviteCloudPath = getConnectInviteCloudPath( url.hostname, url.pathname, diff --git a/router/test/agent-chat.test.ts b/router/test/agent-chat.test.ts index 7631ddcd..3893cdbc 100644 --- a/router/test/agent-chat.test.ts +++ b/router/test/agent-chat.test.ts @@ -1,6 +1,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import worker, { getAgentChatCloudPath, getAgentPagePath, getLegacyAgentPageRedirect, getShortHostRedirect, prefersHtmlOverMarkdown } from "../index.js"; +import worker, { + getAgentChatCloudPath, + getAgentPagePath, + getLegacyAgentPageRedirect, + getShortHostRedirect, + isRelayAgentRegistryRoute, + prefersHtmlOverMarkdown, +} from "../index.js"; const ID = "0123456789abcdef0123456789abcdef"; @@ -28,6 +35,15 @@ describe("router agent chat", () => { } }); + it("accepts any handle-shaped chat slug on arelay.to but keeps agentrelay.com scoped", () => { + expect(getAgentChatCloudPath("arelay.to", `/acme-support/${ID}`, "POST")) + .toBe(`/cloud/api/v1/agent-chat/acme-support/${ID}`); + expect(getAgentChatCloudPath("www.arelay.to", `/acme-support/${ID}`, "POST")) + .toBe(`/cloud/api/v1/agent-chat/acme-support/${ID}`); + expect(getAgentChatCloudPath("agentrelay.com", `/acme-support/${ID}`, "POST")) + .toBeUndefined(); + }); + it("only claims POSTs", () => { for (const method of ["GET", "HEAD", "OPTIONS", "DELETE"]) { expect(getAgentChatCloudPath("arelay.to", `/agent-relay/${ID}`, method)).toBeUndefined(); @@ -45,14 +61,52 @@ describe("router agent chat", () => { expect(new URL(cloud.fetch.mock.calls[0][0].url).pathname).toBe(`/cloud/api/v1/agent-chat/agent-relay/${ID}`); }); - it("leaves the page, the bridge script, unknown agents and other hosts alone", () => { + it("leaves the page, bridge script, malformed handles and other hosts alone", () => { expect(getAgentChatCloudPath("arelay.to", "/agent-relay", "POST")).toBeUndefined(); expect(getAgentChatCloudPath("arelay.to", "/agent-relay/bridge.sh", "POST")).toBeUndefined(); - expect(getAgentChatCloudPath("arelay.to", `/someone-else/${ID}`, "POST")).toBeUndefined(); + expect(getAgentChatCloudPath("arelay.to", `/Not-A-Handle/${ID}`, "POST")).toBeUndefined(); expect(getAgentChatCloudPath("arelay.to", `/agent-relay/${ID.toUpperCase()}`, "POST")).toBeUndefined(); expect(getAgentChatCloudPath("example.com", `/agent-relay/${ID}`, "POST")).toBeUndefined(); }); + it("forwards the guide and bounded registry API paths only on arelay.to", async () => { + expect(isRelayAgentRegistryRoute("arelay.to", "/register", "GET")).toBe(true); + expect(isRelayAgentRegistryRoute("arelay.to", "/register/", "GET")).toBe(true); + expect(isRelayAgentRegistryRoute("arelay.to", "/register", "POST")).toBe(false); + expect(isRelayAgentRegistryRoute("arelay.to", "/api/v1/registrations", "POST")).toBe(true); + expect(isRelayAgentRegistryRoute("arelay.to", "/api/v1/registrations/id/verify", "POST")).toBe(true); + expect(isRelayAgentRegistryRoute("arelay.to", "/api/v1/agents/acme/manage", "PATCH")).toBe(true); + expect(isRelayAgentRegistryRoute("arelay.to", "/api/v1/registrations-legacy", "GET")).toBe(false); + expect(isRelayAgentRegistryRoute("www.arelay.to", "/register", "GET")).toBe(false); + expect(isRelayAgentRegistryRoute("agentrelay.com", "/api/v1/agents/acme", "GET")).toBe(false); + + const cloud = { fetch: vi.fn(async () => new Response("wrong upstream")) }; + const relayAgent = { + fetch: vi.fn(async (_request: Request) => Response.json({ handle: "acme" })), + }; + const response = await worker.fetch( + new Request("https://arelay.to/api/v1/agents/acme?format=json"), + buildEnv(cloud, { RELAY_AGENT_WORKER: relayAgent }), + ctx, + ); + expect(response.status).toBe(200); + expect(cloud.fetch).not.toHaveBeenCalled(); + const forwarded = relayAgent.fetch.mock.calls[0][0]; + expect(forwarded.url).toBe("https://arelay.to/api/v1/agents/acme?format=json"); + }); + + it("fails registry routes closed when relay-agent is not configured", async () => { + const cloud = { fetch: vi.fn(async () => new Response("wrong upstream")) }; + const response = await worker.fetch( + new Request("https://arelay.to/register"), + buildEnv(cloud), + ctx, + ); + expect(response.status).toBe(503); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(cloud.fetch).not.toHaveBeenCalled(); + }); + it("forwards a conversation POST, body intact, to the cloud worker", async () => { const cloud = { fetch: vi.fn(async (_request: Request) => new Response("agent-relay: hi", { status: 200 })) }; const response = await worker.fetch( From 34afc02a517d3083d1e1b680602ce0c0e86ae46f Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Wed, 7 Oct 2026 17:35:09 -0700 Subject: [PATCH 2/5] Fail closed for registry-owned chat routes Session-Id: 01a1176f-b97d-7942-a94a-61f5b8b38013 Session-Id: 01a1176f-b97d-7942-a94a-61f5b8b38013 --- router/index.ts | 5 ++++- router/test/agent-chat.test.ts | 13 ++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/router/index.ts b/router/index.ts index 360c563b..a78e023d 100644 --- a/router/index.ts +++ b/router/index.ts @@ -574,6 +574,9 @@ export default { url.pathname, request.method, ); + const registryOwnedChatRoute = Boolean( + agentChatCloudPath && AGENT_CHAT_PATH.exec(url.pathname)?.[1] !== "agent-relay", + ); const relayAgentRoute = Boolean(agentChatCloudPath || registryRoute); const shortHostRedirect = relayAgentRoute || agentPagePath ? undefined @@ -645,7 +648,7 @@ export default { // Unlike the legacy agent-relay chat route, registry endpoints have no // Cloud fallback. Keep them off the marketing origin when the dedicated // Worker is absent during a rollback or unavailable environment. - if (registryRoute) { + if (registryRoute || registryOwnedChatRoute) { return new Response("The agent registry is unavailable. Retry shortly.\n", { status: 503, headers: { diff --git a/router/test/agent-chat.test.ts b/router/test/agent-chat.test.ts index 3893cdbc..9750aedb 100644 --- a/router/test/agent-chat.test.ts +++ b/router/test/agent-chat.test.ts @@ -97,13 +97,20 @@ describe("router agent chat", () => { it("fails registry routes closed when relay-agent is not configured", async () => { const cloud = { fetch: vi.fn(async () => new Response("wrong upstream")) }; - const response = await worker.fetch( + const registryResponse = await worker.fetch( new Request("https://arelay.to/register"), buildEnv(cloud), ctx, ); - expect(response.status).toBe(503); - expect(response.headers.get("cache-control")).toBe("no-store"); + const companyChatResponse = await worker.fetch( + new Request(`https://arelay.to/acme-support/${ID}`, { method: "POST", body: "hello" }), + buildEnv(cloud), + ctx, + ); + expect(registryResponse.status).toBe(503); + expect(registryResponse.headers.get("cache-control")).toBe("no-store"); + expect(companyChatResponse.status).toBe(503); + expect(companyChatResponse.headers.get("cache-control")).toBe("no-store"); expect(cloud.fetch).not.toHaveBeenCalled(); }); From 40d24a229cddd72b8f1258ee08cee1b6d8cb5ba0 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Wed, 7 Oct 2026 17:47:07 -0700 Subject: [PATCH 3/5] Clarify registry routing failures Session-Id: 01a1176f-b97d-7942-a94a-61f5b8b38013 Session-Id: 01a1176f-b97d-7942-a94a-61f5b8b38013 --- router/README.md | 7 ++++--- router/index.ts | 13 ++++++++++--- router/test/agent-chat.test.ts | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/router/README.md b/router/README.md index 2798fe20..27ae790e 100644 --- a/router/README.md +++ b/router/README.md @@ -5,9 +5,10 @@ Route precedence: - `GET`/`HEAD /connect/` (Relay Connect invite links) is served by the Cloud app's `/cloud/connect/` route. - `/observer/file*` routes to the RelayFile observer app, expected to be built with `basePath: "/observer/file"`. - `/observer*` routes to the Relaycast observer app. -- On `arelay.to`, `POST //<32hex>`, `GET /register`, and - `/api/v1/(registrations|agents)/**` route to the `relay-agent` Worker. The - Worker owns registry lookup and its 404/410 decisions. +- On `arelay.to`, `GET`/`HEAD /register` and + `/api/v1/(registrations|agents)/**` route to the `relay-agent` Worker; + `POST //<32hex>` routes there on both `arelay.to` and + `www.arelay.to`. The Worker owns registry lookup and its 404/410 decisions. - everything else falls back to the legacy proxy target. RelayFile observer deployment is wired from the production job in diff --git a/router/index.ts b/router/index.ts index a78e023d..be401e13 100644 --- a/router/index.ts +++ b/router/index.ts @@ -230,7 +230,8 @@ export function isRelayAgentRegistryRoute( method: string, ): boolean { if (hostname !== SHORT_HOST) return false; - if ((pathname === "/register" || pathname === "/register/") && method === "GET") { + if ((pathname === "/register" || pathname === "/register/") + && (method === "GET" || method === "HEAD")) { return true; } return REGISTRY_API_PATH.test(pathname); @@ -633,7 +634,10 @@ export default { : "relay_agent_upstream_failed", message: error instanceof Error ? error.message : "unknown error", })); - return new Response("The agent chat is unavailable. Retry the same command shortly.\n", { + const unavailableMessage = registryRoute + ? "The agent registry is unavailable. Retry shortly.\n" + : "The agent chat is unavailable. Retry the same command shortly.\n"; + return new Response(unavailableMessage, { status: 503, headers: { "content-type": "text/plain; charset=utf-8", @@ -649,7 +653,10 @@ export default { // Cloud fallback. Keep them off the marketing origin when the dedicated // Worker is absent during a rollback or unavailable environment. if (registryRoute || registryOwnedChatRoute) { - return new Response("The agent registry is unavailable. Retry shortly.\n", { + const unavailableMessage = registryRoute + ? "The agent registry is unavailable. Retry shortly.\n" + : "The agent chat is unavailable. Retry the same command shortly.\n"; + return new Response(unavailableMessage, { status: 503, headers: { "content-type": "text/plain; charset=utf-8", diff --git a/router/test/agent-chat.test.ts b/router/test/agent-chat.test.ts index 9750aedb..adbaa9a4 100644 --- a/router/test/agent-chat.test.ts +++ b/router/test/agent-chat.test.ts @@ -71,6 +71,7 @@ describe("router agent chat", () => { it("forwards the guide and bounded registry API paths only on arelay.to", async () => { expect(isRelayAgentRegistryRoute("arelay.to", "/register", "GET")).toBe(true); + expect(isRelayAgentRegistryRoute("arelay.to", "/register", "HEAD")).toBe(true); expect(isRelayAgentRegistryRoute("arelay.to", "/register/", "GET")).toBe(true); expect(isRelayAgentRegistryRoute("arelay.to", "/register", "POST")).toBe(false); expect(isRelayAgentRegistryRoute("arelay.to", "/api/v1/registrations", "POST")).toBe(true); @@ -109,8 +110,10 @@ describe("router agent chat", () => { ); expect(registryResponse.status).toBe(503); expect(registryResponse.headers.get("cache-control")).toBe("no-store"); + expect(await registryResponse.text()).toContain("agent registry is unavailable"); expect(companyChatResponse.status).toBe(503); expect(companyChatResponse.headers.get("cache-control")).toBe("no-store"); + expect(await companyChatResponse.text()).toContain("agent chat is unavailable"); expect(cloud.fetch).not.toHaveBeenCalled(); }); @@ -223,6 +226,21 @@ describe("router agent chat", () => { expect(logged).toHaveBeenCalledWith(expect.stringContaining("relay_agent_upstream_failed")); }); + it("uses registry-specific wording when the registry upstream fails", async () => { + const cloud = { fetch: vi.fn(async () => new Response("wrong upstream")) }; + const relayAgent = { fetch: vi.fn(async () => { throw new Error("down"); }) }; + vi.spyOn(console, "error").mockImplementation(() => undefined); + const response = await worker.fetch( + new Request("https://arelay.to/api/v1/agents/acme"), + buildEnv(cloud, { RELAY_AGENT_WORKER: relayAgent }), + ctx, + ); + + expect(response.status).toBe(503); + expect(await response.text()).toContain("agent registry is unavailable"); + expect(cloud.fetch).not.toHaveBeenCalled(); + }); + it("sends the short host's root, www and cloud app to agentrelay.com", () => { expect(getShortHostRedirect(new URL("https://arelay.to/"))).toBe("https://agentrelay.com/"); expect(getShortHostRedirect(new URL("https://arelay.to/cloud/teams?x=1"))).toBe("https://agentrelay.com/cloud/teams?x=1"); From b9746c02ae1048fe91a546b1424b014a6e9303e8 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Wed, 7 Oct 2026 18:36:51 -0700 Subject: [PATCH 4/5] Generalize agent page routing by handle Session-Id: 01a1176f-b97d-7942-a94a-61f5b8b38013 --- router/index.ts | 8 +++++--- router/test/agent-chat.test.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/router/index.ts b/router/index.ts index be401e13..12bcf1fe 100644 --- a/router/index.ts +++ b/router/index.ts @@ -43,10 +43,11 @@ const CONNECT_INVITE_PATH = /^(\/connect\/[A-Za-z0-9_-]{32,64}(?:\.(?:json|md))? // site, but its bare root and the signed-in cloud app send people to agentrelay.com. const SHORT_HOST = "arelay.to"; const SHORT_HOST_WWW = "www.arelay.to"; +const HANDLE_SEGMENT = "[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])"; // A visitor's coding agent chats through //<32 hex id>. arelay.to // accepts every handle-shaped slug and lets relay-agent's registry decide // whether it exists. agentrelay.com retains its established agent-relay route. -const AGENT_CHAT_PATH = /^\/([a-z0-9-]+)\/([0-9a-f]{32})\/?$/; +const AGENT_CHAT_PATH = new RegExp(`^/(${HANDLE_SEGMENT})/([0-9a-f]{32})/?$`); const REGISTRY_API_PATH = /^\/api\/v1\/(?:registrations|agents)(?:\/|$)/; const WEBHOOK_ORIGIN_FLAG_KEY = "WEBHOOK_ORIGIN"; export const WILL_CALENDAR_URL = "https://calendar.app.google/RqLuQyT3dYe5e2YdA"; @@ -159,9 +160,9 @@ export function getAgentPagePath( } let agent: string | undefined; if (hostname === SHORT_HOST || hostname === SHORT_HOST_WWW) { - agent = /^\/([a-z0-9-]+)\/?$/.exec(pathname)?.[1]; + agent = new RegExp(`^/(${HANDLE_SEGMENT})/?$`).exec(pathname)?.[1]; } else if (hostname === PRIMARY_HOST) { - agent = /^\/u\/([a-z0-9-]+)\/?$/.exec(pathname)?.[1]; + agent = new RegExp(`^/u/(${HANDLE_SEGMENT})/?$`).exec(pathname)?.[1]; } if (!agent) { return undefined; @@ -220,6 +221,7 @@ export function getLegacyAgentPageRedirect(url: URL): string | undefined { return agent === "agent-relay" ? `https://${PRIMARY_HOST}/u/${agent}${url.search}` : undefined; +} // Registration and management are served directly by relay-agent. The public // API matcher is prefix-bounded so lookalikes such as /registrations-legacy do diff --git a/router/test/agent-chat.test.ts b/router/test/agent-chat.test.ts index adbaa9a4..01ef139f 100644 --- a/router/test/agent-chat.test.ts +++ b/router/test/agent-chat.test.ts @@ -253,9 +253,12 @@ describe("router agent chat", () => { const browser = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; expect(getAgentPagePath("arelay.to", "/agent-relay", "GET", browser)).toBe("/u/agent-relay"); expect(getAgentPagePath("agentrelay.com", "/u/agent-relay", "GET", browser)).toBe("/u/agent-relay"); + expect(getAgentPagePath("arelay.to", "/acme-support", "GET", browser)).toBe("/u/acme-support"); + expect(getAgentPagePath("agentrelay.com", "/u/acme-support", "GET", browser)).toBe("/u/acme-support"); for (const accept of ["*/*", null, "text/markdown", "text/markdown, text/html;q=0.9, */*;q=0.8", "text/plain"]) { expect(getAgentPagePath("arelay.to", "/agent-relay", "GET", accept)).toBe("/u/agent-relay/agent.md"); expect(getAgentPagePath("agentrelay.com", "/u/agent-relay/", "GET", accept)).toBe("/u/agent-relay/agent.md"); + expect(getAgentPagePath("arelay.to", "/acme-support", "GET", accept)).toBe("/u/acme-support/agent.md"); } }); @@ -304,9 +307,10 @@ describe("router agent chat", () => { expect(put).not.toHaveBeenCalled(); }); - it("leaves other paths, unknown agents, POSTs and other hosts alone", () => { + it("leaves other paths, invalid handles, POSTs and other hosts alone", () => { expect(getAgentPagePath("arelay.to", "/agent-relay/bridge.sh", "GET", "*/*")).toBeUndefined(); - expect(getAgentPagePath("arelay.to", "/someone-else", "GET", "*/*")).toBeUndefined(); + expect(getAgentPagePath("arelay.to", "/-invalid", "GET", "*/*")).toBeUndefined(); + expect(getAgentPagePath("arelay.to", "/x", "GET", "*/*")).toBeUndefined(); expect(getAgentPagePath("arelay.to", "/agent-relay", "POST", "*/*")).toBeUndefined(); expect(getAgentPagePath("agentrelay.com", "/agent-relay", "GET", "*/*")).toBeUndefined(); expect(getAgentPagePath("example.com", "/u/agent-relay", "GET", "*/*")).toBeUndefined(); From fed8b90146d8f431f14dcb55757abd28b17337ec Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Wed, 7 Oct 2026 18:43:29 -0700 Subject: [PATCH 5/5] Preserve reserved short-host routes Session-Id: 01a1176f-b97d-7942-a94a-61f5b8b38013 --- router/index.ts | 9 ++++++++- router/test/agent-chat.test.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/router/index.ts b/router/index.ts index 12bcf1fe..9632cb11 100644 --- a/router/index.ts +++ b/router/index.ts @@ -44,6 +44,13 @@ const CONNECT_INVITE_PATH = /^(\/connect\/[A-Za-z0-9_-]{32,64}(?:\.(?:json|md))? const SHORT_HOST = "arelay.to"; const SHORT_HOST_WWW = "www.arelay.to"; const HANDLE_SEGMENT = "[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])"; +// Keep this aligned with relay-agent's registry reservation set. agent-relay is +// the one seeded exception; every other reserved site/API slug must retain its +// router meaning instead of being rewritten as a company profile. +const RESERVED_AGENT_PAGE_HANDLES = new Set([ + "register", "connect", "api", "cloud", "admin", "www", "help", "support", + "docs", "status", "security", "abuse", "login", "signup", +]); // A visitor's coding agent chats through //<32 hex id>. arelay.to // accepts every handle-shaped slug and lets relay-agent's registry decide // whether it exists. agentrelay.com retains its established agent-relay route. @@ -164,7 +171,7 @@ export function getAgentPagePath( } else if (hostname === PRIMARY_HOST) { agent = new RegExp(`^/u/(${HANDLE_SEGMENT})/?$`).exec(pathname)?.[1]; } - if (!agent) { + if (!agent || RESERVED_AGENT_PAGE_HANDLES.has(agent)) { return undefined; } const wantsHtml = method === "HEAD" || prefersHtmlOverMarkdown(accept); diff --git a/router/test/agent-chat.test.ts b/router/test/agent-chat.test.ts index 01ef139f..17841247 100644 --- a/router/test/agent-chat.test.ts +++ b/router/test/agent-chat.test.ts @@ -94,6 +94,14 @@ describe("router agent chat", () => { expect(cloud.fetch).not.toHaveBeenCalled(); const forwarded = relayAgent.fetch.mock.calls[0][0]; expect(forwarded.url).toBe("https://arelay.to/api/v1/agents/acme?format=json"); + + const register = await worker.fetch( + new Request("https://arelay.to/register", { headers: { accept: "text/html" } }), + buildEnv(cloud, { RELAY_AGENT_WORKER: relayAgent }), + ctx, + ); + expect(register.status).toBe(200); + expect(relayAgent.fetch.mock.calls[1][0].url).toBe("https://arelay.to/register"); }); it("fails registry routes closed when relay-agent is not configured", async () => { @@ -243,6 +251,7 @@ describe("router agent chat", () => { it("sends the short host's root, www and cloud app to agentrelay.com", () => { expect(getShortHostRedirect(new URL("https://arelay.to/"))).toBe("https://agentrelay.com/"); + expect(getShortHostRedirect(new URL("https://arelay.to/cloud"))).toBe("https://agentrelay.com/cloud"); expect(getShortHostRedirect(new URL("https://arelay.to/cloud/teams?x=1"))).toBe("https://agentrelay.com/cloud/teams?x=1"); expect(getShortHostRedirect(new URL("https://www.arelay.to/agent-relay"))).toBe("https://arelay.to/agent-relay"); expect(getShortHostRedirect(new URL("https://arelay.to/agent-relay"))).toBeUndefined(); @@ -311,6 +320,9 @@ describe("router agent chat", () => { expect(getAgentPagePath("arelay.to", "/agent-relay/bridge.sh", "GET", "*/*")).toBeUndefined(); expect(getAgentPagePath("arelay.to", "/-invalid", "GET", "*/*")).toBeUndefined(); expect(getAgentPagePath("arelay.to", "/x", "GET", "*/*")).toBeUndefined(); + for (const reserved of ["register", "connect", "api", "cloud", "admin", "www", "help", "support", "docs", "status", "security", "abuse", "login", "signup"]) { + expect(getAgentPagePath("arelay.to", `/${reserved}`, "GET", "*/*")).toBeUndefined(); + } expect(getAgentPagePath("arelay.to", "/agent-relay", "POST", "*/*")).toBeUndefined(); expect(getAgentPagePath("agentrelay.com", "/agent-relay", "GET", "*/*")).toBeUndefined(); expect(getAgentPagePath("example.com", "/u/agent-relay", "GET", "*/*")).toBeUndefined();