Bots
ClickClack bots are first-class chat identities. They use the same message, thread, DM, upload, search, and realtime surfaces as humans, but their credentials and permissions are explicit, scoped, revocable, and visible in the UI.
This spec covers two bot shapes:
- Service bot: an independent workspace member, not owned by a human. Use
- User bot: a sub-identity owned by a human user. Use for "Peter's
for shared infrastructure agents such as openclaw, deploy-bot, or triage-bot.
OpenClaw bot", personal assistants, or automation that should be visibly attached to a real person and limited to a subset of that person's access.
Both shapes are users with kind=bot. The difference is ownership and maximum permission source.
#Goals
- Humans and bots are distinguishable in API responses and UI.
- Bots can post, reply, read, and subscribe without browser cookies or magic
- User-owned bots cannot exceed their owner's permissions.
- Service bots can exist without a human owner, but only through explicit
- Token scopes are narrow enough for OpenClaw channel extensions and CI agents.
- OpenClaw can run as one service bot and as one user-owned bot in the same
- Crabbox can prove the full path with a desktop/browser/WebVNC demo.
link human sessions.
workspace membership and scoped tokens.
ClickClack workspace.
#Non-Goals
- OAuth app marketplace.
- Slack-compatible app manifests.
- Per-channel ACLs beyond the scopes below.
- Multi-tenant hosted billing or app review.
- Letting bot tokens impersonate arbitrary humans.
#Identity Model
users grows identity metadata:
kind TEXT NOT NULL DEFAULT 'human';
owner_user_id TEXT REFERENCES users(id) ON DELETE CASCADE;
Rules:
kindis eitherhumanorbot.- A human has
owner_user_id = NULL. - A service bot has
kind = botandowner_user_id = NULL. - A user bot has
kind = botandowner_user_id = <human user id>. - A bot can have its own
display_name,handle, andavatar_url. - A bot may not own another bot.
- Deleting a human owner revokes/deletes user-owned bots and tokens.
messages.author_idstays a plainusers.id; old messages keep rendering
even after token revocation.
API User payloads include:
{
"id": "usr_...",
"kind": "bot",
"owner_user_id": "usr_owner...", // omitted for humans and service bots
"display_name": "Peter's OpenClaw",
"handle": "peter-openclaw"
}
UI:
- Profile panes show a
Botbadge for all bots. - User-owned bot profiles show
Bot of <owner>; the current UI uses the owner - Service bot profiles show
Service bot. - Sidebar People can either include bots with badges or split into
Peopleand
user ID until owner-profile hydration lands.
Bots; the API must expose enough metadata for either UI.
#Token Model
Bot tokens are bearer credentials with a ccb_ prefix. The server stores only a SHA-256 hash.
bot_tokens (
id TEXT PRIMARY KEY,
token_hash TEXT NOT NULL UNIQUE,
bot_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
owner_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
name TEXT NOT NULL,
scopes_json TEXT NOT NULL,
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL,
last_used_at TEXT,
revoked_at TEXT
)
Rules:
- Raw token is returned once on creation.
- Token auth resolves to the bot user, never to the owner.
owner_user_idon the token is copied from the bot at creation for audit.- Revoked tokens fail auth.
- Tokens update
last_used_atafter successful auth. - Tokens are workspace-scoped. A token cannot access another workspace even if
- A bot may have multiple tokens for different runtimes.
the bot user is later added there.
#Permission Scopes
MVP scopes:
workspaces:readchannels:readchannels:writemessages:readmessages:writethreads:readthreads:writedms:readdms:writerealtime:readuploads:writeprofile:read
Derived bundles:
bot:read= workspace/channel/message/thread/DM/realtime read scopes.bot:write= read scopes plus message/thread/DM/upload write scopes.bot:admin= all MVP scopes including channel creation/update.
Enforcement:
- Human sessions keep today's membership-based behavior.
- Bot-token requests check membership, token workspace, and scope.
- User-owned bots additionally require the owner to still be a member of the
- Service bots require their own workspace membership.
- Store-level membership remains the final data access guard.
target workspace.
MVP endpoint mapping:
GET /api/me:profile:readGET /api/workspaces*:workspaces:readGET /api/workspaces/{id}/channels:channels:readPOST/PATCH channel endpoints:channels:writeGET channel/DM/thread messages: matching read scopePOST channel/DM/thread messages: matching write scopeGET /api/realtime/eventsand/ws:realtime:readPOST /api/uploads:uploads:writePOST /api/messages/{id}/attachments:uploads:writeandmessages:writePOST /api/realtime/ephemeral:messages:write; theagent.progressPATCH /api/me: human sessions only; bot tokens cannot mutate profiles.
event type additionally requires a bot token and exactly one concrete target (channel_id or direct_conversation_id), never workspace-wide. DM progress also requires dms:write.
#Creation Surfaces
CLI MVP:
clickclack admin bot create \
--workspace wsp_... \
--name "OpenClaw Service" \
--handle openclaw \
--scopes bot:write \
--plain
clickclack admin bot create \
--workspace wsp_... \
--owner usr_peter \
--name "Peter's OpenClaw" \
--handle peter-openclaw \
--scopes bot:write \
--plain
Plain output returns the raw token only. JSON output returns {bot, token, bot_token}.
For the practical install flow, including Docker commands and OpenClaw configuration, see Bot installs.
HTTP API:
POST /api/workspaces/{workspace_id}/botsGET /api/workspaces/{workspace_id}/botsGET /api/bots/{bot_user_id}/tokensPOST /api/bots/{bot_user_id}/tokensPOST /api/bot-tokens/{token_id}/revoke
The creation and token-management API requires a human session. Bot tokens can read and write through the normal chat APIs according to scope, but cannot mint, list, or revoke bot tokens.
POST /api/workspaces/{workspace_id}/bots returns {bot, bot_token}. The bot_token.token field is the one-time raw ccb_... token and is never returned by list calls. GET /api/workspaces/{workspace_id}/bots returns {bots: [{bot, tokens}]} with token metadata redacted. Rotation is create-new, move the runtime, then revoke-old through POST /api/bot-tokens/{token_id}/revoke.
#Runtime Contract
Long-running bots should:
- Authenticate with
Authorization: Bearer ccb_.... - Resolve workspace/channel IDs through normal APIs.
- Backfill durable events through
/api/realtime/events?after_cursor=.... - Connect to
/api/realtime/ws?workspace_id=...&after_cursor=.... - Persist the latest cursor after each processed event.
- Ignore events authored by their own
bot_user_id. - Post replies through normal message/thread/DM endpoints.
The TypeScript SDK exposes ClickClackBot, a light runner around ClickClackClient and the realtime WebSocket:
const bot = new ClickClackBot({
baseUrl,
token,
workspaceId,
afterCursor,
onEvent: async (event, client) => {
if (event.type === "message.created" && event.channel_id) {
await client.channels.sendMessage(event.channel_id, { body: "ack" });
}
},
});
bot.start();
Runtimes are still responsible for reconnect backoff, cursor persistence, and own-message filtering.
#OpenClaw Extension
OpenClaw should ship a ClickClack channel extension that treats ClickClack as a normal chat transport.
Channel id: clickclack.
Config:
{
"channels": {
"clickclack": {
"baseUrl": "https:
"$CLICKCLACK_BOT_TOKEN",
"clickclack",
"channel:general",
["*"]
}
}
}
Target grammar:
channel:<name-or-id>dm:<user-id-or-handle>thread:<message-id>
Inbound:
- The extension subscribes to ClickClack realtime events.
message.createdevents become OpenClaw inbound turns.- Events authored by the configured bot user are ignored.
- Channel messages map to OpenClaw group/channel turns.
- DMs map to direct turns.
- Thread replies preserve the source thread/message ID.
Outbound:
- OpenClaw replies post through ClickClack message/thread/DM endpoints.
- Thread context routes back to the originating ClickClack thread when present.
- The extension stores the latest realtime cursor per account.
The live proof must configure two ClickClack accounts:
openclaw-service: independent service bot.peter-openclaw: user-owned bot with Peter as owner.
#Crabbox Live Proof
Success criteria:
- ClickClack tests pass locally and in CI-shaped gates.
- OpenClaw extension tests pass in the chosen OpenClaw checkout.
- A Crabbox managed Linux lease is created with
--desktop --browser. - ClickClack runs with bot identities and tokens.
- OpenClaw runs with an OpenAI API key and both ClickClack bot accounts.
- WebVNC opens the browser to ClickClack.
- The visible chat shows both bots distinctly:
- one service bot
- one bot of Peter
- A screenshot is captured with
crabbox screenshot. - The screenshot is inspected and confirms the expected chat/browser state.
Preferred Crabbox flow:
crabbox warmup --desktop --browser
crabbox run --id <lease> -- ./scripts/run-clickclack-openclaw-bot-smoke.sh
crabbox desktop launch --id <lease> --browser --url http://127.0.0.1:8080/app --webvnc --open
crabbox screenshot --id <lease> --output .artifacts/clickclack-bots-webvnc.png
Secrets:
- Load
OPENAI_API_KEYfrom the local environment or approved secret source. - Do not print bot tokens, session tokens, or OpenAI keys.
- Store raw bot tokens only in ephemeral Crabbox env files for the live test.
#Migration Plan
- Add user kind/owner migration and backfill all existing users as humans.
- Add bot token table and store methods.
- Extend auth resolution to return actor metadata and token scopes.
- Gate bot-token requests by scope in HTTP handlers.
- Add CLI bot creation.
- Add
kind/owner_user_idto OpenAPI, SDK, and UI. - Add SDK bot runner and example.
- Add OpenClaw ClickClack channel extension.
- Add local and Crabbox live tests.