# Overview
> What sideagent is and the two ways to run it: the open-source library or sideagent Cloud.
Source: https://sideagent.dev/docs
sideagent embeds a chat agent in your app. You give it an OpenAPI spec and an OpenAI-compatible LLM host and key. It gives you a secure agent endpoint and a React chat UI. The agent calls your API on behalf of the signed-in user, using that user's own credentials.
## Two ways to run it
- **sideagent** (open source, MIT): a TypeScript library you run in your own server. See [Server](https://sideagent.dev/docs/server.md) and [Chat UI](https://sideagent.dev/docs/chat-ui.md).
- **sideagent Cloud**: the same agent, hosted. You configure it in a dashboard and embed it with a publishable key. See [sideagent Cloud](https://sideagent.dev/docs/cloud.md).
## How a request flows
1. The browser sends a chat message to the agent endpoint, with the user's `Authorization` header.
2. The server verifies the user ([User authentication](https://sideagent.dev/docs/auth.md)) and loads the agent.
3. The model picks operations from your allowlist ([Operations](https://sideagent.dev/docs/operations.md)).
4. Calls that change data pause for the user's approval ([Approvals](https://sideagent.dev/docs/approvals.md)).
5. Allowed calls go to your API with the user's own credentials, and the result streams back to the chat.
## Where to go next
- New here: [Quickstart](https://sideagent.dev/docs/quickstart.md).
- Hosted setup, no server code: [sideagent Cloud](https://sideagent.dev/docs/cloud.md).
- What protects you and your users: [Security model](https://sideagent.dev/docs/security.md).
- Building your own client: [Wire protocol](https://sideagent.dev/docs/protocol.md).
## For LLMs and tools
Every page here is also available as Markdown: add `.md` to the page URL, for example `/docs/quickstart.md`. [`/llms.txt`](https://sideagent.dev/llms.txt) lists all pages. [`/llms-full.txt`](https://sideagent.dev/llms-full.txt) has the whole documentation in one file.
---
# Quickstart
> Install sideagent, mount the agent on one route and render the chat in React.
Source: https://sideagent.dev/docs/quickstart
Install the package:
```bash
npm i sideagent
```
Create the agent on your server and mount its handler on one route:
```ts
import { createAgent, honoHandler } from "sideagent/server";
const agent = await createAgent({
spec: "https://api.example.com/openapi.json",
baseUrl: "https://api.example.com",
llm: { baseUrl: "https://api.openai.com/v1", apiKey: process.env.LLM_API_KEY!, model: "gpt-4o-mini" },
allow: ["listInvoices", "createInvoice"],
allowedOrigins: ["https://app.example.com"],
getAuth: (req) => ({
userId: "user-123",
headers: { authorization: req.headers.get("authorization")! },
}),
});
app.on(["POST", "OPTIONS"], "/agent", honoHandler(agent));
```
Render the chat in your React app:
```tsx
import { AgentChat } from "sideagent";
```
## Try it without an API key
Clone the repo and run `pnpm demo`. It starts a fake invoices API, an agent with a mock LLM, and the UI.
## Next steps
- Tune the options: [Server](https://sideagent.dev/docs/server.md).
- Choose what the model may call: [Operations and allowlist](https://sideagent.dev/docs/operations.md).
- Replace the hard-coded `userId`: [User authentication](https://sideagent.dev/docs/auth.md).
- Skip the server: [sideagent Cloud](https://sideagent.dev/docs/cloud.md).
---
# Server
> createAgent options, adapters for Hono, Next.js and Express, and the doctor check.
Source: https://sideagent.dev/docs/server
`createAgent` is async: it parses the spec and, if you configured knowledge, embeds it. Create the agent once and reuse it. It returns an object with a request `handler` and a `doctor()` check.
## Options
| Option | What it does |
| --- | --- |
| `spec` | OpenAPI 3.0 or 3.1: a URL, an object or a JSON string. |
| `baseUrl` | Pinned API host. The spec's `servers` list is ignored. |
| `llm` | `baseUrl`, `apiKey` and `model` of any OpenAI-compatible host. |
| `allow` | Default-deny allowlist of operation IDs, or `tag:name`. |
| `allowedOrigins` | Browser origins that may call the endpoint. |
| `getAuth` | Returns the user ID and the headers to forward to your API. |
| `approvalSecret` | Signs approval tokens. Set it on serverless or multi-instance deployments, so every instance verifies the same tokens. |
| `checkpointer`, `threadStore` | Persistent chat history. See [Chat history](https://sideagent.dev/docs/history.md). |
| `knowledge`, `vectorStore` | Markdown and URL sources the agent can search. See [Knowledge](https://sideagent.dev/docs/knowledge.md). |
## Adapters
`honoHandler(agent)`, `nextRoute(agent)` and `expressHandler(agent)`. The core `agent.handler(request)` takes a standard `Request` and returns a `Response`.
## Doctor
Call `await agent.doctor()` at startup. It reports what the agent can do (operations loaded, knowledge sources, warnings) and checks the LLM connection.
---
# Operations and allowlist
> Which API operations the model can call, and the x-agent hints you can add to your spec.
Source: https://sideagent.dev/docs/operations
Every operation in your spec with an `operationId` can become a tool. Only the ones you allow are exposed to the model. List IDs, or use `tag:reports` to allow a whole tag. New operations are never allowed by default.
## The x-agent extension
Add hints to the spec with the `x-agent` extension on an operation:
```json
"x-agent": {
"approval": "required",
"description": "Better text for the model than the summary",
"invalidates": ["invoices"],
"responseFields": ["id", "total"]
}
```
- `approval`: force or skip the approval step. See [Approvals](https://sideagent.dev/docs/approvals.md).
- `description`: what the model sees, in place of the spec's summary.
- `invalidates`: tags the UI should refetch after the call succeeds (`hooks.onInvalidate` in the chat component).
- `responseFields`: only these fields of the response reach the model and the browser.
## Limits on calls
Calls use the pinned `baseUrl` and never follow redirects. Request and response sizes, the number of steps per turn and the time per turn are capped.
---
# User authentication
> How getAuth identifies the signed-in user and forwards their credentials to your API.
Source: https://sideagent.dev/docs/auth
The agent acts as the signed-in user. In `getAuth` you verify the request the way your API does, and return:
```ts
getAuth: async (req) => {
const user = await verifySession(req); // your code; return null to reject
if (!user) return null;
return { userId: user.id, headers: { authorization: req.headers.get("authorization")! } };
}
```
- `userId` scopes chat threads: a user can only see their own history.
- `headers` are forwarded to your API on every tool call, so your API enforces its normal permissions. The agent never has more access than the user.
## Credentials must travel in Authorization
The browser can only send `content-type` and `authorization` to the endpoint, so credentials must travel in `Authorization`. Cookies are not forwarded.
---
# Approvals
> How calls that change data pause for the user, and how approval tokens are bound to the exact arguments.
Source: https://sideagent.dev/docs/approvals
Calls that change data (anything that is not `GET`) need the user's approval. The agent pauses and the UI shows the exact call: operation, path and arguments. The user approves or rejects.
- The server issues a signed approval token bound to the exact arguments. Changing an argument invalidates the token.
- Tokens expire. The UI restores a pending approval after a page refresh.
- Set `approvalSecret` yourself when more than one instance serves the agent.
To force or skip the step for one operation, use `x-agent.approval` ([Operations](https://sideagent.dev/docs/operations.md)).
---
# Knowledge
> Give the agent documents it can search, from Markdown or public URLs, with a vector store.
Source: https://sideagent.dev/docs/knowledge
Give the agent documents it can search, so it answers questions about your product and not only your API.
```ts
createAgent({
// ...
knowledge: {
embeddings: { model: "text-embedding-3-small" },
sources: [{ id: "guide", title: "Invoicing guide", content: markdownString }],
urls: ["https://docs.example.com/payment-terms", { url: "https://example.com/shipping", title: "Shipping" }],
},
vectorStore, // optional: pgvector, Vectorize, ...
});
```
The agent gets a `search_knowledge` tool. Answers show a Sources card, with links only for URLs you configured.
## Behavior
- Passages reach the model as labelled untrusted data and never reach the browser.
- URLs come only from config. They must be public http(s) and follow only same-host redirects. `maxSourceBytes` caps their size.
- URL sources refresh in the background after `ttlMs` (default one hour), or when you call `agent.knowledge.refresh()`.
- A source that fails to load is skipped with a warning, and shown in `doctor()`.
- The default store is in memory, so every start embeds all sources again. Pass a persistent `vectorStore` for large knowledge.
---
# Chat UI
> The AgentChat and AgentProvider React components, the docked sidebar and result views.
Source: https://sideagent.dev/docs/chat-ui
The `sideagent` package exports React components. React 18 or later is an optional peer dependency.
```tsx
import { AgentChat } from "sideagent";
tag === "invoices" && refetchInvoices() }}
/>
```
## Docked sidebar
Wrap your app in `AgentProvider` to dock the chat on the right, beside the app instead of over it. It takes the same props as `AgentChat`. The user drags the panel's left edge, or uses the arrow keys on it, to resize; the width is remembered.
```tsx
import { AgentProvider } from "sideagent";
```
Anything under `AgentProvider` can call `useAgent()` to get `{ open, setOpen }`, for example an "Ask assistant" button in a toolbar.
## Results
Tool results render as readable views. Arrays of objects become tables (up to 50 rows and 8 columns), objects become labelled fields, and errors get their own state. Raw JSON stays under "Details". Everything is plain text, never HTML. Override a view per operation:
```tsx
}} />
```
---
# Chat history
> Persistent threads with a checkpointer and a threadStore, and the History button in the chat.
Source: https://sideagent.dev/docs/history
`AgentChat` has a History button (list, open, delete), reopens the last conversation after a refresh, and restores a pending approval.
Messages live in your LangGraph `checkpointer`. The list of threads lives in a `threadStore`, in memory by default. Both must be persistent in production:
```ts
createAgent({ /* ... */ checkpointer: new PostgresSaver(/* ... */), threadStore: myThreadStore });
```
A `ThreadStore` implements `list`, `has`, `touch` and `delete`. History is only shown from the user's own index.
---
# Theming
> Match the chat to your design system with CSS variables and light or dark themes.
Source: https://sideagent.dev/docs/theming
The chat is neutral by default: grayscale, your font, a 10px radius.
`theme="auto"` follows the OS and a host `.dark`, `data-theme="dark"` or `data-bs-theme="dark"`. Pass `"light"` or `"dark"` to force one.
Match your design system with CSS variables:
```css
:root {
--sideagent-accent: var(--primary);
--sideagent-accent-fg: var(--primary-foreground);
--sideagent-bg: var(--background);
--sideagent-fg: var(--foreground);
--sideagent-border: var(--border);
--sideagent-surface: var(--muted);
--sideagent-muted: var(--muted-foreground);
--sideagent-radius: 10px;
--sideagent-font: inherit;
}
```
---
# Wire protocol
> The request bodies and server-sent events the agent endpoint speaks.
Source: https://sideagent.dev/docs/protocol
The endpoint takes `POST` with a JSON body. A chat turn answers with server-sent events; history actions answer with JSON.
## Requests
```ts
{ "threadId"?: string, "message": string, "context"?: unknown }
{ "threadId": string, "approval": { "token": string, "decision": "approve" | "reject" } }
{ "action": "threads.list" }
{ "action": "threads.get", "threadId": string }
{ "action": "threads.delete", "threadId": string }
```
## Events in a turn
| Event | Meaning |
| --- | --- |
| `turn.start` | Carries the `threadId`. |
| `text.delta` | A piece of the assistant's text. |
| `tool.start` | The agent began calling an operation. |
| `approval.request` | The call needs approval. Carries the arguments, a token and an expiry. |
| `tool.complete` | The call finished: HTTP status, arguments, result, and tags to invalidate. |
| `tool.rejected` | The user rejected the call. |
| `error` | A `code` and a `message`. |
| `turn.end` | The turn is over. |
---
# Security model
> Default deny, user credentials, approvals, pinned host, limits, origins and redaction.
Source: https://sideagent.dev/docs/security
- **Default deny.** Only allowlisted operations are exposed to the model.
- **User credentials.** Your API sees the signed-in user's own credentials, never a service account.
- **Approvals.** Non-`GET` calls need server-verified approval, bound to the exact arguments.
- **Pinned host.** API calls go to `baseUrl` only. SSRF guards block private and metadata addresses, and redirects are not followed.
- **Limits.** Size, step and time limits apply to every turn.
- **Origins.** Browser calls from origins you did not list are refused.
- **Threads.** Chat history is bound to the user and only shown to them.
- **Redaction.** Audit events are redacted. LLM keys, approval secrets and user tokens do not appear in logs or errors.
- **Untrusted text.** Knowledge passages and API results are passed to the model as data, and rendered as plain text, never HTML.
---
# sideagent Cloud
> The hosted version: configure an agent in a dashboard and embed it with a publishable key.
Source: https://sideagent.dev/docs/cloud
sideagent Cloud runs the agent for you. You do not host a server or write `createAgent` code.
## Set up an agent
1. [Sign in](https://sideagent.dev/app) with your email. You get a magic link.
2. Create an agent. Each agent has its own configuration, history, limits and audit log.
3. Add instructions, knowledge sources (URLs or pasted text) and, optionally, your OpenAPI spec and the operations to allow.
4. Pick an LLM: the built-in model, or any OpenAI-compatible host with your own key. Keys are encrypted at rest and shown only as their last four characters.
5. Pick how users are identified: token verification (JWKS or a userinfo URL), or anonymous for public, knowledge-only chat.
6. Add the origins that may embed the chat, then turn the agent on.
## Keys
Each agent has a publishable key, safe in the browser.
The publishable key names the agent and the sideagent Cloud host. Pass it to the chat (sideagent 0.4.0 or later):
```tsx
```
The chat sends requests to the agent the key names and carries the key in the `x-sideagent-key` header. Every request to the agent endpoint needs it: without the key the answer is `401`, and with a key for another agent or host it is `404`, as for a missing agent. The key proves nothing on its own.
## Signed-in users
With token verification the chat sends your user's login token, and the agent calls your API with that same token. You write no server code. Pass a function that returns the current token (sideagent 0.5.0 or later):
```tsx
({ Authorization: `Bearer ${await getToken()}` })}
storageKey={user.id}
/>
```
`getToken` is your login SDK's call, for example `getAccessTokenSilently()` in Auth0 or `getToken()` in Clerk. The chat calls it before every request, so it never sends an expired token. `storageKey` keeps each user's last conversation apart when several people share a browser.
The agent checks every request:
- **JWKS**: the token is a JWT signed with a key from your JWKS URL, with an asymmetric algorithm you allowed (RS, PS, ES or EdDSA; never `none` or HS256). `iss` must equal the issuer, `aud` must contain the audience, and `exp` must be present and in the future (60 seconds of clock skew). The user is the claim you chose, `sub` by default. The JWKS is cached for ten minutes; a token with a new key ID fetches it again, at most once a minute.
- **Userinfo URL**: the agent calls your userinfo URL with the token. A `2xx` JSON answer names the user in the field you chose; `401` or `403` refuses the token. The answer is cached for the time you set, 0 to 300 seconds, so a revoked token can keep working that long.
A missing, malformed or refused token gets `401 {"error":"unauthorized"}`, whatever the cause. The cause is in the agent's audit log: `no_token`, `bad_token`, `token_rejected`, `jwks_unavailable` or `userinfo_unavailable`. When the JWKS or the userinfo URL cannot be reached, every request is refused.
Your API receives `Authorization: Bearer ` and no other header from the browser. Cookies are never forwarded, so an API that accepts only a session cookie cannot be called as the user.
Threads and approvals belong to the user who started them. The user ID is the issuer (or the userinfo URL's origin) plus the claim, so two issuers with the same `sub` stay apart.
## Knowledge-only agents
Knowledge-only agents work without a spec. They answer only from their knowledge and refuse unrelated questions. This documentation is the knowledge for the assistant on the [home page](https://sideagent.dev/), which is a sideagent Cloud agent.
## Public agents and abuse limits
An anonymous agent is open to anyone who can load your page, so sideagent Cloud limits it:
- **Origin and key.** Requests must come from an origin you allowed and carry the publishable key. Both are public, so they stop mistakes, not a script that sets the same headers.
- **Rate limit.** Ten requests a minute for each visitor address and agent.
- **Daily cap.** Anonymous visitors share a daily message cap for each agent. When it is reached, or when your account's monthly cap for the built-in model is reached, the agent answers `429 {"error":"limit_reached"}`.
- **Hidden chat.** The agent reports when it is out of messages. With sideagent 0.8.0 or later the chat then renders nothing, launcher included, until the next page load. Under `AgentProvider`, `useAgent().available` is `false`, so you can hide your own "ask" button.
- **Output length.** A reply from the built-in model is cut at a fixed number of tokens.
### Bot challenge
sideagent Cloud can also require a [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) challenge from anonymous visitors. The visitor does nothing in most cases: the challenge runs in the background, and a box appears only when Cloudflare needs the visitor to interact. Once solved, a token valid for 30 minutes goes in the `x-sideagent-challenge` header of every request. A request without a valid token gets `403 {"error":"challenge_required"}`.
The chat does not do this on its own. Copy the helper below into your app and pass its result as `getHeaders` (sideagent 0.5.0 or later). It does nothing when the agent asks for no challenge, so adding it before the challenge is switched on is safe. It costs one extra `agent.info` request per page load.
```ts
// 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 | undefined;
const loadTurnstile = () =>
(loading ??= new Promise((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 {
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((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> {
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 | undefined;
let token: { value: string; expires: number } | null = null;
let running: Promise | 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 = {};
if (token && token.expires > Date.now()) headers[CHALLENGE_HEADER] = token.value;
return headers;
};
}
```
```tsx
const getHeaders = useMemo(() => challengeHeaders("pk_live_…"), []);
```
If your site sets a Content-Security-Policy, allow `https://challenges.cloudflare.com` in `script-src` and `frame-src`. An agent that verifies user tokens never asks for a challenge; only anonymous visitors are challenged. If you already pass `getHeaders` for a token, merge the two results.
## Status
sideagent Cloud is in development. The dashboard, agent configuration and knowledge indexing are available. The agent endpoint serves anonymous, knowledge-only agents, and agents with token verification: API calls and approvals with the built-in model, history for signed-in users. Not yet available: public operations for anonymous users, and API calls with your own LLM key.
---
# Preview
> Try your agent from the dashboard before it goes live: knowledge answers, operations, and why a token fails.
Source: https://sideagent.dev/docs/preview
Every agent page has a preview. It opens the same chat your users get, docked beside the page, and talks to your saved settings. You can use it before the agent goes live.
## What it does
- **Knowledge.** The Sources card lists the passages the agent found, with their scores. A passage under the relevance floor still shows up, so you can see why an answer came back as "off topic". Edit a source, re-index, and ask again.
- **Operations.** Give the preview a test token and an agent with allowed operations can call your API. Reads call your API. Writes ask for approval as usual, but after you approve nothing is sent: the card says so, and the agent is told. The preview never changes data in your API.
- **Tokens.** For agents that verify sign-in tokens (JWKS or userinfo), paste a token from your own app and choose **Check token**. If it fails, the preview says why: not a valid token, rejected (issuer, audience, expiry or signing key), or the key or userinfo URL did not answer. Your users only ever see a plain 401. The token stays in the browser tab. It is not saved.
- **Settings.** The preview tests the **saved** settings and shows which version. Unsaved edits are not included. A new version starts a new chat.
## Good to know
- Preview chats are kept apart from your users' chats and are removed with **Clear preview chats** or when you delete the agent.
- Preview messages count against the built-in model's monthly limit. With your own model key they do not.
- Without a token, an agent that verifies tokens answers from its knowledge only and cannot call your API.
- Preview traffic is recorded in the audit log as `preview.*` events, so it does not mix with your real traffic.