Bridle
Skill CleanSlice/skills/bridle
CleanSlice agent skills — architecture patterns, vertical slices, conventional commits for Claude Code and AI coding agents.
npx -y skills add CleanSlice/skills --skill bridleAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Embed Bridle webchat into a website. Wire the SDK, mint embed JWTs server-side, theme the widget through Shadow DOM, and connect to a hub running your agent.
SKILL.md
12.2 KB, as published. Nobody here has run it
Bridle
Bridle is an embeddable webchat for AI agents — one script tag connects a browser to an agent process through a stateless WebSocket hub. There are three integration paths (drop-in script, npm, headless), one wire protocol, and one auth model with two flavors (public agent vs. JWT).
Source: https://github.com/CleanSlice/bridle · Docs: https://bridle.cleanslice.org · npm: @cleanslice/bridle
Architecture (always the same)
Browser ─► Bridle Hub (NestJS) ─► Agent Runtime
│ │ /ws/client │ /ws/agent
│ │ JWT + agentId │ apiKey + agentId
│ └───────────────────────┘
│ routes by agentId, no state stored
▼
SDK auto-mounts <bridle-chat> Custom Element with Shadow DOM
The hub is stateless — it just routes messages between matched (agentId) pairs. Sessions persist in the agent runtime, not the hub.
Quick Reference
| Need | Answer |
|---|---|
| Drop-in script | https://bridle.cleanslice.org/sdk/latest.js |
| Pinned version | /sdk/v0.js (latest 0.x) or /sdk/v0.8.0.js (exact) |
| npm package | @cleanslice/bridle (exports init, BridleClient, types) |
| Custom Element tag | <bridle-chat> |
| Auth modes | Public (origin whitelist, no token) · JWT (browser sends JWT minted by your backend) |
| Theme tokens | CSS vars on bridle-chat (e.g. --bridle-primary) — pierce Shadow DOM |
| Internal selectors | .bridle__panel, .bridle__bubble, etc. — only reachable via customCss (v0.8.0+) |
Integration path 1 — Drop-in <script>
The simplest option. One tag, no build step. Works on any HTML page (WordPress, Webflow, Shopify, plain HTML).
<script
src="https://bridle.cleanslice.org/sdk/latest.js"
data-api-url="https://your-hub.example.com"
data-agent-id="agent-abc-123"
data-token="<jwt>"
></script>
When this loads, the SDK registers a <bridle-chat> Custom Element and mounts a floating bubble in the bottom-right corner.
Available data-* attributes
| Attribute | Default | Purpose |
|---|---|---|
data-agent-id | required | Agent identifier registered on the hub |
data-token | required unless public agent | JWT minted by your backend |
data-api-url | inferred from script's origin | Hub origin |
data-mode | floating | floating (FAB) or inline (mounted inside data-mount) |
data-mount | <body> | CSS selector for inline mode |
data-title | Agent Chat | Header text |
data-placeholder | Type a message... | Input placeholder |
data-theme | default | default or cleanslice (teal palette) |
data-color-mode | auto | auto / light / dark |
data-custom-css | — | Inline CSS injected into the shadow root (v0.8.0+) |
data-stylesheet | — | One or more CSS file URLs (comma-separated) loaded inside the shadow root (v0.8.0+) |
data-prompt | — | Extra context forwarded with every message at handshake |
Integration path 2 — npm (bundler)
For Vite/Next/Nuxt/Webpack. You get tree-shaking, type defs, and full programmatic control.
npm i @cleanslice/bridle
import { init } from '@cleanslice/bridle'
const chat = init({
apiUrl: 'https://your-hub.example.com',
agentId: 'agent-abc-123',
token: () => fetchJwt(), // string OR async function for refresh
mount: '#chat', // CSS selector or HTMLElement
mode: 'inline',
title: 'Support',
themeVars: { '--bridle-primary': '#0070f3' },
customCss: `.bridle__panel { border-radius: 5px; }`,
onReady: () => console.log('connected'),
onMessage: (msg) => console.log(msg.text),
onError: (err) => console.error(err),
})
chat.sendMessage('Hi!')
chat.open()
chat.close()
chat.destroy()
init() returns { element, open, close, sendMessage, destroy }. The token can be a string OR a function returning string | Promise<string> so you can refresh expired JWTs without remounting.
Integration path 3 — Headless (no UI)
When you want to bring your own UI but use the hub for transport.
import { BridleClient } from '@cleanslice/bridle'
const client = new BridleClient({
apiUrl: 'https://your-hub.example.com',
agentId: 'agent-abc-123',
token: 'eyJhbG...',
})
client.on('message', (m) => render(m.text))
client.on('stream', (m) => render(m.text)) // partial text as it streams
client.on('stream_end', (m) => finalize(m.text))
client.on('error', (err) => handleAuthError(err))
await client.connect()
client.send('hello')
Authentication
Pick public for marketing sites, docs, demos. Pick JWT when the user is signed into your product and you want per-user routing/quota/identity.
Public agent (origin whitelist, no token)
Configure the agent on the hub to accept connections from specific origins. The browser sends no token; the hub accepts the WebSocket based on the request's Origin header.
Use this when:
- The site is public and there's no per-user state to scope by
- All visitors get the same agent treatment
The SDK call simply omits data-token / token. If the origin isn't whitelisted, the connection is rejected and the SDK shows an inline "Origin … isn't whitelisted" banner.
JWT minted server-side
The integrator's backend mints a short-lived JWT and hands it to the browser. The Ranch hub provides this endpoint at POST /auth/embed/token:
// On YOUR backend (Node example, no dotenv):
const res = await fetch(`${RANCH_API_URL}/auth/embed/token`, {
method: 'POST',
headers: {
Authorization: `Bearer ${RANCH_API_KEY}`, // server-side key, never to browser
'Content-Type': 'application/json',
},
body: JSON.stringify({
sub: user.id, // becomes JWT.sub → clientId on hub
email: user.email,
expiresIn: '7d', // <n>(s|m|h|d). Default 15m is short for dev.
}),
})
const { data } = await res.json() // ranch wraps as { success, data }
return data.token // string + expiresAt
The JWT carries sub (becomes clientId for message routing) and email. Pass roles: ['ADMIN'] to mint an admin token (clientId = 'admin').
Never put RANCH_API_KEY in the browser. The whole point of the mint endpoint is to keep it server-side. Expose only the minted JWT.
Theming
The Custom Element uses Shadow DOM, so host-page CSS doesn't reach internal classes. Two levers:
CSS variables (cross the shadow boundary)
| Variable | Purpose |
|---|---|
--bridle-primary | Brand color (FAB, send button, user bubbles) |
--bridle-primary-fg | Foreground on primary |
--bridle-bg / --bridle-fg | Panel background / body text |
--bridle-bubble-bg | Assistant bubble background |
--bridle-user-bg / --bridle-user-fg | User bubble |
--bridle-border | Borders, dividers |
--bridle-radius | Panel + bubble corner radius |
--bridle-shadow | Panel/FAB shadow |
--bridle-z | z-index of the floating panel |
--bridle-font | Font family |
Three ways to set them:
/* Plain CSS (host page) */
bridle-chat { --bridle-primary: #ec4899; --bridle-radius: 8px; }
// init() option
init({ ..., themeVars: { '--bridle-primary': '#ec4899' } })
Pre-built palettes: theme: 'default' | 'cleanslice' (cleanslice = teal/cyan).
customCss (inject inside Shadow DOM, v0.8.0+)
For everything CSS variables can't express — overriding .bridle__panel's border, the .bridle__bubble font size, etc.:
init({
...,
customCss: `
.bridle__panel {
border-radius: 5px;
box-shadow: 0 2px 6px #00000029;
border: 1px solid #C5D5FF;
}
.bridle__bubble { font-size: 13px; }
`,
stylesheets: ['/css/bridle-overrides.css'], // or comma-list via data-stylesheet
})
Cascade order: your overrides are appended after the component's own <style>, so equal-specificity rules win without !important.
Internal classes (part of the public surface)
| Class | What it is |
|---|---|
.bridle__panel | The chat panel |
.bridle__header | Header row with title and close button |
.bridle__messages | Scrollable message list |
.bridle__bubble | A single message bubble |
.bridle__bubble--md | Assistant bubble with rendered Markdown |
.bridle__msg--user / .bridle__msg--assistant | Bubble wrapper per role |
.bridle__input | Footer composer |
.bridle__fab | Floating action button |
.bridle__typing | Three-dot typing indicator |
.bridle__banner--error | Connection-error banner |
These class names are part of the SDK's public surface — they won't be renamed without a major-version bump.
Message Parts (wire protocol)
All messages carry parts: BridlePart[] — text, image, file:
{ type: 'text', text: 'Hello' }
{ type: 'image', base64: '...', mediaType: 'image/jpeg' }
{ type: 'file', url: 'https://...', name: 'doc.pdf', mimeType: 'application/pdf' }
The text field is always present as a shorthand. Legacy clients sending { text, images } are auto-converted by the hub.
Streaming uses stream events (accumulated text, not deltas — easier for clients) and a final stream_end event.
Recipe: embed bridle into a customer site
Paste-ready prompt the agent can adapt:
- Decide auth mode. Public site / docs / marketing → public agent (no token). Signed-in product → JWT.
- For public: ask the operator to whitelist the customer's origin on the agent in Ranch admin UI.
- For JWT: add a server-side endpoint that calls
POST {RANCH_API_URL}/auth/embed/tokenwith the API key as a Bearer header. Return only{ token, expiresAt }to the browser. - Mount the SDK. For static HTML, drop the
<script>tag. For SPA, callinit()in a single mount-on-load lifecycle hook (onMounted,useEffect, etc.). Always pair withdestroy()on unmount. - Match brand. Start with
themeVarsfor color/radius/font. Drop tocustomCssonly when you need to restyle internal classes. - Verify. Open the page; the FAB should appear in the corner (floating) or inline in the mount. The "●" status dot in the header should be green within ~1 second.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Failed to start bridle: fetch failed | Hub isn't reachable from the SDK's backend (token mint) — usually means the hub process is down or RANCH_API_URL is wrong | Start the hub, fix the URL |
Banner: Origin … isn't whitelisted for this agent | Public agent's allowed-origin list doesn't include this site | Add the origin in the admin UI |
Banner: MISSING_TOKEN | Non-public agent and no token was passed | Pass data-token / token |
Banner: INVALID_TOKEN | JWT signature failed (wrong JWT_SECRET) or expired | Re-mint a fresh token; verify JWT_SECRET matches between mint backend and hub |
Banner: MISSING_AGENT_ID | data-agent-id / agentId missing | Set it |
| Chat connects, you send a message, nothing happens | No agent runtime is online for this agentId | Start the agent process / paddock; check /api/agent/<agentId>/health for agentConnected: true |
| Host-page CSS doesn't reach the panel | Shadow DOM isolation by design | Use themeVars for variables, customCss / stylesheets for everything else (v0.8.0+) |
| FAB never appears (drop-in) | Script 404 (/sdk/latest.js), or no data-agent-id (script skips auto-mount) | Check Network tab; verify data-agent-id is present |
Don't
- Don't put
RANCH_API_KEY/ hubJWT_SECRETin the browser. Both are server-side only. - Don't rely on host-page CSS reaching
.bridle__*classes — usecustomCssinstead. - Don't
init()more than once per mount. Usedestroy()first if you need to recreate. - Don't hardcode
data-api-urlif the SDK is served from the same origin as the hub — it's inferred from the script src. - Don't mount in floating mode AND inline mode simultaneously for the same agent unless you intentionally want two independent sessions (each gets its own clientId).