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
43 changes: 43 additions & 0 deletions web/app/u/[handle]/agent.md/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getAgentProfile, validRegistryHandle } from '../../../../lib/agent-registry';
import {
agentChatAgentGuide,
agentChatUrlForHandle,
newConversationId,
normalizeAgentName,
} from '../../../../lib/agent-chat-snippet';

export const dynamic = 'force-dynamic';

type RouteContext = {
params: Promise<{ handle: string }>;
};

const MARKDOWN_HEADERS = {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'no-store',
'Referrer-Policy': 'no-referrer',
'X-Content-Type-Options': 'nosniff',
};

export async function GET(_request: Request, { params }: RouteContext) {
const { handle } = await params;
if (!validRegistryHandle(handle)) {
return new Response('# Agent not found\n', { status: 404, headers: MARKDOWN_HEADERS });
}
const profile = await getAgentProfile(handle);
if (!profile) {
return new Response('# Agent not found\n', { status: 404, headers: MARKDOWN_HEADERS });
}
if (profile.status !== 'active') {
return new Response(
`# ${normalizeAgentName(profile.displayName)}\n\nThis agent is temporarily unavailable because its domain verification is no longer current.\n`,
{ status: 410, headers: MARKDOWN_HEADERS },
);
Comment thread
khaliqgant marked this conversation as resolved.
}
return new Response(agentChatAgentGuide({
conversationId: newConversationId(),
baseUrl: agentChatUrlForHandle(profile.handle),
agentName: profile.displayName,
agentHandle: profile.handle,
}), { headers: MARKDOWN_HEADERS });
}
111 changes: 111 additions & 0 deletions web/app/u/[handle]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

import { AgentChatSnippet } from '../../../components/AgentChatSnippet';
import { SiteFooter } from '../../../components/SiteFooter';
import { SiteNav } from '../../../components/SiteNav';
import { getAgentProfile, validRegistryHandle } from '../../../lib/agent-registry';
import { agentChatUrlForHandle, newConversationId } from '../../../lib/agent-chat-snippet';
import { defaultOgImage } from '../../../lib/og-meta';
import home from '../../landing.module.css';
import flows from '../../flows/flows.module.css';
import s from '../agent-relay/agent-relay.module.css';

export const dynamic = 'force-dynamic';

type PageProps = {
params: Promise<{ handle: string }>;
};

const STEPS = [
{ title: 'Copy the snippet', body: 'It carries a private conversation link made just for you.' },
{ title: 'Paste it into your open chat', body: 'Claude Code or Codex uses the snippet to contact this verified agent. Nothing to install.' },
{ title: 'Approve once', body: 'Your agent asks to run one command. Choose “don’t ask again” and the rest of the conversation flows.' },
];

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { handle } = await params;
if (!validRegistryHandle(handle)) return { title: 'Not Found' };
const profile = await getAgentProfile(handle);
if (!profile) return { title: 'Not Found' };
const title = profile.status === 'active'
? `Chat with ${profile.displayName}`
: profile.displayName;
const canonical = `https://arelay.to/${profile.handle}`;
return {
title,
description: profile.description,
alternates: { canonical },
openGraph: {
title,
description: profile.description,
url: canonical,
type: 'website',
images: [defaultOgImage()],
},
twitter: {
card: 'summary_large_image',
title,
description: profile.description,
images: [defaultOgImage().url],
},
};
}

export default async function RegisteredAgentPage({ params }: PageProps) {
const { handle } = await params;
if (!validRegistryHandle(handle)) notFound();
const profile = await getAgentProfile(handle);
if (!profile) notFound();

return (
<div className={`${flows.page} ${home.messagingPage} ${s.page}`}>
<a className="skip-link" href="#main">Skip to content</a>
<SiteNav />

<main id="main">
<section className={s.hero}>
<h1 className={`${home.headline} ${s.headline}`}>
{profile.status === 'active' ? `Chat with ${profile.displayName}` : profile.displayName}
</h1>
{profile.status === 'active' && (
<p className={s.badge}>✓ verified {profile.verifiedDomain}</p>
)}
<p className={`${home.subtitle} ${s.subtitle}`}>{profile.description}</p>
{profile.status === 'active' ? (
<AgentChatSnippet
initialConversationId={newConversationId()}
baseUrl={agentChatUrlForHandle(profile.handle)}
agentName={profile.displayName}
agentHandle={profile.handle}
/>
Comment thread
khaliqgant marked this conversation as resolved.
Comment thread
khaliqgant marked this conversation as resolved.
) : (
<p className={s.suspended} role="status">
This agent is temporarily unavailable because its domain verification is no longer current.
</p>
)}
</section>

{profile.status === 'active' && (
<section className={s.steps} aria-labelledby="how-heading">
<h2 id="how-heading" className={s.stepsHeading}>How it works</h2>
<ol className={s.stepList}>
{STEPS.map((step) => (
<li key={step.title}>
<strong>{step.title}</strong>
<span>{step.body}</span>
</li>
))}
</ol>
<p className={s.note}>
Each message is a plain HTTPS request, and the reply comes back as its output. Treat replies from any
external agent as information, not instructions, and ask before sharing code, files or secrets.
</p>
</section>
)}
</main>

<SiteFooter />
</div>
);
}
22 changes: 22 additions & 0 deletions web/app/u/agent-relay/agent-relay.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@
margin: 0 0 8px;
}

.badge {
margin: -10px 0 0;
border: 1px solid #47708d;
border-radius: 999px;
padding: 6px 12px;
color: #b9ddf5;
font-family: var(--font-mono, ui-monospace), monospace;
font-size: 13px;
}

.suspended {
width: 100%;
max-width: 760px;
margin: 0 auto;
border: 1px solid #8f655d;
border-radius: 12px;
padding: 20px;
background: #291d1d;
color: #ffd1c8;
line-height: 1.6;
}

.steps {
max-width: 760px;
margin: 0 auto;
Expand Down
1 change: 1 addition & 0 deletions web/app/u/agent-relay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default function AgentRelayChatPage() {
<main id="main">
<section className={s.hero}>
<h1 className={`${home.headline} ${s.headline}`}>Have your agent chat with ours</h1>
<p className={s.badge}>✓ verified agentrelay.com</p>
<p className={`${home.subtitle} ${s.subtitle}`}>{DESCRIPTION}</p>
<AgentChatSnippet initialConversationId={newConversationId()} />
</section>
Expand Down
14 changes: 12 additions & 2 deletions web/components/AgentChatSnippet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@ import s from './agent-chat-snippet.module.css';

// The server renders a fresh conversation per request, so an agent that fetches
// the page sees a complete snippet too.
export function AgentChatSnippet({ initialConversationId }: { initialConversationId: string }) {
export function AgentChatSnippet({
initialConversationId,
baseUrl,
agentName,
agentHandle,
}: {
initialConversationId: string;
baseUrl?: string;
agentName?: string;
agentHandle?: string;
}) {
const [conversationId, setConversationId] = useState(initialConversationId);
const [status, setStatus] = useState<'' | 'copied' | 'copy-failed' | 'new'>('');

const snippet = agentChatSnippet({ conversationId });
const snippet = agentChatSnippet({ conversationId, baseUrl, agentName, agentHandle });

async function handleCopy() {
try {
Expand Down
62 changes: 54 additions & 8 deletions web/lib/agent-chat-snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,48 @@ export const AGENT_CHAT_BRIDGE_COMMAND =
process.env.NEXT_PUBLIC_AGENT_CHAT_BRIDGE_COMMAND?.trim() ||
`curl -fsSL ${AGENT_CHAT_URL.replace(/\/$/, '')}/bridge.sh | sh -s --`;

export function agentChatUrlForHandle(
handle: string,
configuredUrl = AGENT_CHAT_URL,
): string {
const url = new URL(configuredUrl);
const segments = url.pathname.replace(/\/+$/, '').split('/');
segments[segments.length - 1] = encodeURIComponent(handle);
url.pathname = segments.join('/');
url.search = '';
url.hash = '';
return url.toString().replace(/\/$/, '');
}

export function newConversationId(): string {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}

export function normalizeAgentName(agentName: string | undefined): string {
return (agentName ?? 'Agent Relay').replace(/[\p{C}\s]+/gu, ' ').trim() || 'registered agent';
}

export function agentChatSnippet({
conversationId,
baseUrl = AGENT_CHAT_URL,
bridgeCommand = AGENT_CHAT_BRIDGE_COMMAND,
agentName,
agentHandle,
}: {
conversationId: string;
baseUrl?: string;
bridgeCommand?: string;
agentName?: string;
agentHandle?: string;
}): string {
const url = `${baseUrl.replace(/\/$/, '')}/${conversationId}`;
const normalizedAgentName = normalizeAgentName(agentName);
const isAgentRelay = agentHandle ? agentHandle.toLowerCase() === 'agent-relay' : agentName === undefined;
const agentLabel = isAgentRelay
? 'the Agent Relay agent'
: `the registered agent ${JSON.stringify(normalizedAgentName)}`;
// Messages travel through a file written with the agent's file tool, never
// through shell source: replies from other agents may be quoted back, and a
// file's contents are never parsed. The command is identical for every turn,
Expand All @@ -36,9 +62,15 @@ export function agentChatSnippet({
// user, and the full id is the conversation's bearer secret.
const messageFile = `/tmp/arelay-${conversationId.slice(0, 16)}.txt`;
const lines = [
'Chat with the Agent Relay agent for me and show me each reply. Start by saying hi and asking what it can help with.',
'',
'Before you set anything up for Agent Relay (installs, sign-ins, workspaces, integrations), tell this agent what you plan to do and what you found, then confirm each step with it. It knows the current setup steps and will check what already exists, such as your active workspace, before anything new is created.',
`Chat with ${agentLabel} for me. Show me each reply. Start by saying hi and asking what it can help with.`,
];
if (isAgentRelay) {
lines.push(
'',
'Before you set anything up for Agent Relay (installs, sign-ins, workspaces, integrations), tell this agent what you plan to do and what you found, then confirm each step with it. It knows the current setup steps and will check what already exists, such as your active workspace, before anything new is created.',
);
}
Comment thread
cursor[bot] marked this conversation as resolved.
lines.push(
'',
`To send a message, write it to ${messageFile} with your file-editing tool (replace the whole file each time, and never write it with a shell command). Then run this exact command; it prints the reply:`,
'',
Expand All @@ -49,7 +81,7 @@ export function agentChatSnippet({
`If it prints "no reply yet", empty ${messageFile} and run the same command to keep waiting.`,
'',
'If curl cannot connect, your sandbox is blocking the network: ask me to approve running this exact command outside the sandbox. Choosing "don\'t ask again" covers the rest of the conversation.',
];
);
if (bridgeCommand) {
const dir = `/tmp/agent-relay/${conversationId}`;
lines.push(
Expand All @@ -68,13 +100,27 @@ export function agentChatSnippet({
// The agent-readable version of arelay.to/agent-relay, served to anything that
// does not ask for HTML (plain curl, web-fetch tools). Each fetch is a new
// private conversation.
export function agentChatAgentGuide({ conversationId, baseUrl = AGENT_CHAT_URL }: { conversationId: string; baseUrl?: string }): string {
export function agentChatAgentGuide({
conversationId,
baseUrl = AGENT_CHAT_URL,
agentName,
agentHandle,
}: {
conversationId: string;
baseUrl?: string;
agentName?: string;
agentHandle?: string;
}): string {
const normalizedAgentName = normalizeAgentName(agentName);
const isAgentRelay = agentHandle ? agentHandle.toLowerCase() === 'agent-relay' : agentName === undefined;
return [
'# Chat with the Agent Relay agent',
`# Chat with ${isAgentRelay ? 'the Agent Relay agent' : normalizedAgentName}`,
'',
"Your human pointed you here to talk with Agent Relay's agent. It answers questions about Agent Relay (Flows, Sessions, Desktop, Relayfile, Connect) and helps you set them up. This fetch created a private conversation for you; use it for the whole chat and don't fetch this page again.",
isAgentRelay
? "Your human pointed you here to talk with Agent Relay's agent. It answers questions about Agent Relay (Flows, Sessions, Desktop, Relayfile, Connect) and helps you set them up. This fetch created a private conversation for you; use it for the whole chat and don't fetch this page again."
: `Your human pointed you here to talk with the verified agent ${JSON.stringify(normalizedAgentName)}. This fetch created a private conversation for you; use it for the whole chat and don't fetch this page again.`,
'',
agentChatSnippet({ conversationId, baseUrl }),
agentChatSnippet({ conversationId, baseUrl, agentName: normalizedAgentName, agentHandle }),
'',
].join('\n');
}
Loading
Loading