Cloud
sideagent Cloud
Versi ter-hosting: konfigurasikan agent di dasbor dan embed dengan publishable key.
Di halaman ini
sideagent Cloud menjalankan agent untuk Anda. Anda tidak perlu meng-hosting server atau menulis kode createAgent.
Menyiapkan agent
- Masuk dengan email Anda. Anda akan menerima tautan masuk (magic link).
- Buat agent. Setiap agent memiliki konfigurasi, riwayat, batasan, dan audit log sendiri.
- Tambahkan instruksi, knowledge source (URL atau teks yang ditempel), dan, jika perlu, OpenAPI spec Anda beserta operation yang diizinkan.
- Pilih LLM: model built-in, atau host apa pun yang kompatibel dengan OpenAI dengan API key Anda sendiri. Key dienkripsi saat disimpan dan hanya ditampilkan empat karakter terakhirnya.
- Pilih cara pengguna dikenali: token verification (JWKS atau userinfo URL), atau anonim untuk chat publik yang hanya menjawab dari knowledge.
- Tambahkan origin yang boleh meng-embed chat, lalu aktifkan agent.
Key
Setiap agent memiliki satu publishable key yang aman di browser.
Publishable key menyebutkan agent dan host sideagent Cloud. Berikan key ini ke chat (sideagent 0.4.0 atau lebih baru):
<AgentChat publishableKey="pk_live_…" />
Chat mengirim request ke agent yang disebut oleh key dan menyertakan key di header x-sideagent-key. Setiap request ke endpoint agent membutuhkannya: tanpa key jawabannya 401, dan dengan key untuk agent atau host lain jawabannya 404, sama seperti agent yang tidak ada. Key ini sendiri tidak membuktikan apa pun.
Pengguna yang masuk
Dengan token verification, chat mengirim token login pengguna Anda, dan agent memanggil API Anda dengan token yang sama. Anda tidak menulis kode server. Berikan fungsi yang mengembalikan token saat ini (sideagent 0.5.0 atau lebih baru):
<AgentChat
publishableKey="pk_live_…"
getHeaders={async () => ({ Authorization: `Bearer ${await getToken()}` })}
storageKey={user.id}
/>
getToken adalah fungsi SDK login Anda, misalnya getAccessTokenSilently() pada Auth0 atau getToken() pada Clerk. Chat memanggilnya sebelum setiap request, sehingga token yang kedaluwarsa tidak pernah dikirim. storageKey memisahkan percakapan terakhir setiap pengguna ketika beberapa orang memakai browser yang sama.
Agent memeriksa setiap request:
- JWKS: token adalah JWT yang ditandatangani dengan key dari URL JWKS Anda, dengan algoritma asimetris yang Anda izinkan (RS, PS, ES, atau EdDSA; tidak pernah
noneatau HS256).issharus sama dengan issuer,audharus memuat audience, danexpharus ada dan belum lewat (toleransi selisih jam 60 detik). Pengguna adalah claim yang Anda pilih, secara defaultsub. JWKS di-cache selama sepuluh menit; token dengan key ID baru mengambilnya lagi, paling sering sekali per menit. - Userinfo URL: agent memanggil URL userinfo Anda dengan token tersebut. Jawaban JSON
2xxmenyebutkan pengguna pada field yang Anda pilih;401atau403menolak token. Jawaban di-cache selama waktu yang Anda atur, 0 sampai 300 detik, sehingga token yang di-revoke dapat tetap berlaku selama itu.
Token yang tidak ada, rusak, atau ditolak mendapat 401 {"error":"unauthorized"}, apa pun penyebabnya. Penyebabnya ada di audit log agent: no_token, bad_token, token_rejected, jwks_unavailable, atau userinfo_unavailable. Ketika JWKS atau URL userinfo tidak dapat dijangkau, setiap request ditolak.
API Anda menerima Authorization: Bearer <token> dan tidak ada header lain dari browser. Cookie tidak pernah diteruskan, sehingga API yang hanya menerima session cookie tidak dapat dipanggil atas nama pengguna.
Thread dan approval milik pengguna yang memulainya. User ID adalah issuer (atau origin URL userinfo) ditambah claim, sehingga dua issuer dengan sub yang sama tetap terpisah.
Agent khusus knowledge
Agent khusus knowledge berfungsi tanpa spec. Dokumentasi ini adalah knowledge bagi asisten di beranda, yang merupakan agent sideagent Cloud.
Agent publik dan batas penyalahgunaan
Agent anonim terbuka untuk siapa pun yang dapat memuat halaman Anda, sehingga sideagent Cloud membatasinya:
- Origin dan key. Permintaan harus datang dari origin yang Anda izinkan dan membawa publishable key. Keduanya publik, jadi hanya mencegah kesalahan, bukan skrip yang mengirim header yang sama.
- Batas laju. Sepuluh permintaan per menit untuk setiap alamat pengunjung dan agent.
- Batas harian. Pengunjung anonim berbagi batas pesan harian untuk setiap agent. Jika batas itu tercapai, atau batas bulanan akun Anda untuk model built-in tercapai, agent menjawab
429 {"error":"limit_reached"}. - Chat disembunyikan. Agent melaporkan saat pesannya habis. Dengan sideagent 0.8.0 atau lebih baru, chat tidak ditampilkan sama sekali, termasuk tombol peluncurnya, sampai halaman dimuat ulang. Di dalam
AgentProvider,useAgent().availablebernilaifalse, sehingga Anda dapat menyembunyikan tombol "tanya" Anda sendiri. - Panjang jawaban. Jawaban dari model built-in dipotong pada jumlah token yang tetap.
Tantangan bot
sideagent Cloud juga dapat mewajibkan tantangan Cloudflare Turnstile dari pengunjung anonim. Pada umumnya pengunjung tidak perlu melakukan apa pun: tantangan berjalan di latar belakang, dan kotak baru muncul bila Cloudflare memerlukan interaksi pengunjung. Setelah selesai, token yang berlaku 30 menit dikirim di header x-sideagent-challenge pada setiap permintaan. Permintaan tanpa token yang valid mendapat 403 {"error":"challenge_required"}.
Chat tidak melakukan ini sendiri. Salin helper di bawah ke aplikasi Anda dan berikan hasilnya sebagai getHeaders (sideagent 0.5.0 atau lebih baru). Helper ini tidak melakukan apa pun jika agent tidak meminta tantangan, sehingga aman ditambahkan sebelum tantangan diaktifkan. Biayanya satu permintaan agent.info tambahan setiap halaman dimuat.
// challenge-headers.ts
import { parsePublishableKey, publishableEndpoint, PUBLISHABLE_KEY_HEADER } from "sideagent/keys";
const CHALLENGE_HEADER = "x-sideagent-challenge";
const SCRIPT = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
type Turnstile = {
render(el: HTMLElement, o: { sitekey: string; appearance: "interaction-only"; callback: (t: string) => void; "error-callback": () => void; "timeout-callback": () => void }): string;
remove(id: string): void;
};
let loading: Promise<Turnstile> | undefined;
const loadTurnstile = () =>
(loading ??= new Promise<Turnstile>((resolve, reject) => {
const el = document.createElement("script");
el.src = SCRIPT;
el.async = true;
el.onload = () => ((window as any).turnstile ? resolve((window as any).turnstile) : reject(new Error("turnstile missing")));
el.onerror = () => reject(new Error("turnstile blocked"));
document.head.appendChild(el);
}).catch((e) => ((loading = undefined), Promise.reject(e))));
async function solve(siteKey: string): Promise<string> {
const turnstile = await loadTurnstile();
const box = document.createElement("div");
box.style.cssText = "position:fixed;right:16px;bottom:16px;z-index:2147483647";
document.body.appendChild(box);
let id: string | undefined;
try {
return await new Promise<string>((resolve, reject) => {
id = turnstile.render(box, {
sitekey: siteKey,
appearance: "interaction-only",
callback: resolve,
"error-callback": () => reject(new Error("challenge failed")),
"timeout-callback": () => reject(new Error("challenge timed out")),
});
});
} finally {
if (id !== undefined) turnstile.remove(id);
box.remove();
}
}
export function challengeHeaders(publishableKey: string): () => Promise<Record<string, string>> {
const parts = parsePublishableKey(publishableKey);
if (!parts) throw new Error("invalid publishable key");
const endpoint = publishableEndpoint(parts);
const post = (body: unknown) =>
fetch(endpoint, { method: "POST", headers: { "content-type": "application/json", [PUBLISHABLE_KEY_HEADER]: publishableKey }, body: JSON.stringify(body) });
let siteKey: Promise<string | null> | undefined;
let token: { value: string; expires: number } | null = null;
let running: Promise<void> | undefined;
const wanted = () =>
(siteKey ??= post({ action: "agent.info" })
.then(async (r) => (r.ok ? (((await r.json()) as { challenge?: { siteKey?: string } }).challenge?.siteKey ?? null) : null))
.catch(() => null));
const refresh = (key: string) =>
(running ??= (async () => {
const r = await post({ action: "challenge.verify", token: await solve(key) });
if (!r.ok) return;
const j = (await r.json()) as { challengeToken?: string; expiresIn?: number };
if (typeof j.challengeToken === "string") token = { value: j.challengeToken, expires: Date.now() + Number(j.expiresIn) * 1000 };
})()
.catch(() => {})
.finally(() => (running = undefined)));
return async () => {
const key = await wanted();
if (!key) {
siteKey = undefined; // ask again next time, in case the lookup failed
return {};
}
if (!token || token.expires - 60_000 < Date.now()) await refresh(key);
const headers: Record<string, string> = {};
if (token && token.expires > Date.now()) headers[CHALLENGE_HEADER] = token.value;
return headers;
};
}
const getHeaders = useMemo(() => challengeHeaders("pk_live_…"), []);
<AgentChat publishableKey="pk_live_…" getHeaders={getHeaders} />
Jika situs Anda memakai Content-Security-Policy, izinkan https://challenges.cloudflare.com di script-src dan frame-src. Agent yang memverifikasi token pengguna tidak pernah meminta tantangan; hanya pengunjung anonim yang ditantang. Jika Anda sudah memakai getHeaders untuk token, gabungkan kedua hasilnya.
Status
sideagent Cloud sedang dalam pengembangan. Dasbor, konfigurasi agent, dan indexing knowledge sudah tersedia. Endpoint agent melayani agent anonim yang hanya menjawab dari knowledge, dan agent dengan token verification: panggilan API dan approval dengan model built-in, serta riwayat untuk pengguna yang masuk. Belum tersedia: operation publik untuk pengguna anonim, dan panggilan API dengan LLM key Anda sendiri.
