Open
Conversation
…tartup warning, secret catalog
Contributor
Code Review SummaryStatus: 1 Issue Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
Other Observations (not in diff)None. Files Reviewed (17 files)
Reviewed by gpt-5.4-20260305 · 3,344,263 tokens |
…upstream responses
…abort guard, version test)
…base cast
Extract makeClient() helper to eliminate repeated KiloChatClient construction
across the three outbound handlers. Narrow the outbound.base cast from `as never`
to `as { deliveryMode: 'direct'; attachedResults: unknown }` so deliveryMode
remains type-checked. Add FRAGILE comment documenting the SDK-spread dependency.
Kilo Chat is operator-provisioned, not user-configurable. Exposing it in the user-facing secret catalog (which drives the channel-config UI alongside Telegram/Discord/Slack) was wrong: users never bring their own KILOCHAT_API_TOKEN or KILOCHAT_WEBHOOK_SECRET. Move the two secret env vars into INTERNAL_SENSITIVE_ENV_VARS so the sensitive-keys classifier in buildEnvVars still encrypts them, and add a KILOCHAT_ prefix to DENIED_ENV_VAR_PREFIXES so users can't shadow them via custom secrets. Revert the catalog-test additions and drop the UI catalog entry entirely.
File was added in this PR; the alias to KiloChatRouteOptions had no prior consumers to preserve.
The client is new; nothing to preserve compatibility with. sendText was a one-line wrapper around createMessage that dropped the version field to match OpenClaw's outbound adapter return shape. Destructure in the adapter callsite instead and delete the alias + its tests.
pnpm-lock.yaml is only generated at the workspace root in pnpm workspaces, so there's no per-package lockfile to ignore. The sibling kiloclaw-customizer plugin has no .gitignore either.
These handlers lived under `outbound.base.attachedResults` with a narrow cast because the SDK's `ChannelOutboundAdapter` declares no editText/deleteMessage fields. They reached the runtime adapter only because the SDK's `resolveChatChannelOutbound` spreads `outbound.base` verbatim — a JS spread side-effect, not an API. They were also unused: PreviewStream invokes KiloChatClient.editMessage/deleteMessage directly. Edit-in-place streaming is officially a plugin-side concern (confirmed against openclaw/src/channels/plugins/outbound.types.ts and Telegram's reference implementation in extensions/telegram/src/). The onPartialReply callback plus plugin-owned client is the canonical pattern, which we already use via webhook.ts and preview-stream.ts.
Partial-edit streaming is the only intended behavior for this channel, so stop pretending `mode: off | block` exist. Drop readStreamingConfig, the streamingMode branch in buildDeliverWiring, the streamingMode/ throttleMs fields on ResolvedKiloChatAccount, and the associated tests. Throttle stays a module-level constant (500ms) — not user-tunable.
Streaming mode/throttleMs aren't user-tunable; the plugin always streams with a fixed 500ms coalesce window. Keep enabled/baseUrl/dmPolicy/ allowFrom since config-writer still seeds those and OpenClaw validates channels[pluginId] with additionalProperties: false.
Drop the Configure/mode/throttleMs section — the plugin always streams and exposes no knobs. Keep the endpoint contract summary.
The plugin passes no `security` option to createChatChannelPlugin, so
the SDK never invokes resolvePolicy/resolveAllowFrom against the resolved
account (openclaw/src/plugin-sdk/core.ts:538-539 only runs these when
ChatChannelSecurityOptions.dm is configured). And baseUrl is never read
by any plugin code — dispatch hits the controller via KILOCLAW_CONTROLLER_URL
from env, not via the resolved account.
Collapse ResolvedKiloChatAccount to just { accountId } (the SDK requires
that one). Drop readChannelSection, DEFAULT_BASE_URL, the baseUrl/dmPolicy/
allowFrom parsing in resolveAccount, and the matching configSchema entries
in openclaw.plugin.json. Stop writing those fields from config-writer since
nothing consumes them — only `enabled` is meaningful.
Also fix a stale buildConfiguredSecrets test (kiloclaw.test.ts) left over
from the earlier secret-catalog rollback: it still asserted a 'kilo-chat'
key on the configured-secrets map.
The CreatePreviewStreamOptions type exposed setTimer/clearTimer injection hooks for tests, but every test uses vi.useFakeTimers() instead — the seams were never overridden. Inline setTimeout/clearTimeout directly. Also fix a stale channel.test.ts assertion that referenced ResolvedKiloChatAccount.baseUrl, which was removed in the previous account-resolution trim.
…gistration gate Fix A stripped baseUrl from channels['kilo-chat'] because the plugin never reads it. Turns out OpenClaw's `hasMeaningfulChannelConfig` (config-presence.ts:153) requires at least one key other than `enabled` for a channel to count as configured; without it the plugin loads in `setup-runtime` mode instead of `full`, which skips `registerFull(api)` and the /plugins/kilo-chat/webhook HTTP route never registers. Put baseUrl back in the configSchema and config-writer (dmPolicy/ allowFrom stay gone — those are genuinely unread). Caught by local e2e: the webhook curl was returning 404 despite the plugin being loaded.
Steps to exercise the full pipeline on a laptop: build the image, run a fake upstream, start the container with KILOCODE_API_KEY, trigger outbound and inbound flows, observe POST + PATCH for streaming. Covers the gotchas found while bringing this up (registration gate, onboard skip, default-model rejection, proxy-token flag).
Contributor
Code Review SummaryStatus: 6 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
Other Observations (not in diff)None. Files Reviewed (1 files)
Reviewed by gpt-5.4-20260305 · 1,498,742 tokens |
5 tasks
* feat(kilo-chat): scaffold service with wrangler, vitest, drizzle
* feat(kilo-chat): add ULID generation utility
* feat(kilo-chat): add SSE formatting helpers
* feat(kilo-chat): add Drizzle schemas for ConversationDO and MembershipDO
* feat(kilo-chat): add dual auth middleware (JWT + API key)
* feat(kilo-chat): add MembershipDO with conversation index
Per-user/bot Durable Object storing a conversation membership list with
CRUD ops (add, list, update lastMessageId, remove). Includes vitest tests
and updated wrangler types.
* feat(kilo-chat): add ConversationDO with message CRUD
* feat(kilo-chat): add conversation routes (create, list, get)
Implements POST /v1/conversations, GET /v1/conversations, and GET /v1/conversations/:id behind auth middleware, with full integration test coverage.
* feat(kilo-chat): add message routes (create, list, edit, delete)
Implements POST /v1/messages, GET /v1/conversations/:id/messages,
PATCH /v1/messages/:id, and DELETE /v1/messages/:id with membership
checks, webhook queue enqueue for bot members, and MembershipDO
lastMessageId updates.
* feat(kilo-chat): add SSE events endpoint with fan-out and replay
- Add in-memory SSE client tracking and broadcast() to ConversationDO
- Broadcast message.created, message.updated, message.deleted events after each DB write
- Add fetch() handler on ConversationDO to handle /subscribe with member auth
- Support Last-Event-ID replay by querying messages with id > lastEventId
- Add alarm() keepalive that pings all connected clients every 30s
- Add /v1/conversations/:id/events route that forwards to DO's fetch handler
- Tests: access control (403/404), broadcast no-crash, streaming header checks, replay verification
- Streaming tests placed last in file to work around miniflare SQLite WAL isolated storage limitation
* feat(kilo-chat): add typing indicator endpoint with SSE broadcast
* feat(kilo-chat): add webhook queue delivery with HMAC signing
Implements the WEBHOOK_QUEUE consumer that delivers HMAC-SHA256 signed
payloads to the kiloclaw webhook endpoint, with per-message ack/retry
and graceful handling when secrets are not configured.
* fix(kilo-chat): resolve all oxlint errors
* refactor(kilo-chat): replace hand-rolled ULID with ulid package, use monotonicFactory for message IDs
* fix(kilo-chat): review fixes — webhook payload, timing-safe auth, SSE replay, writer cleanup
- Fix webhook queue message shape to match WebhookMessage type (was sending wrong fields)
- Use constant-time comparison for API key auth via crypto.subtle.timingSafeEqual
- SSE replay always sends message.created for missed messages (client never saw them)
- Close dead SSE writers on disconnect to prevent resource leaks
- Remove redundant callerKind guard in message creation
* fix(kilo-chat): review round 2 — timing-safe fix, server-controlled versioning, single webhook
- Remove redundant string-length check in timingSafeEqual (keep byte-length only)
- Server now controls message version (increments from current), not client-supplied
- Send one webhook per message (not one per bot member) since payloads are identical
- Allow version 0 in edit schema for stale clients
* fix(kilo-chat): harden webhook error path against body read failure
* refactor(kilo-chat): rename KILOCHAT_API_KEY to KILOCHAT_API_TOKEN to match kiloclaw
* feat(kilo-chat): add chat.kiloapps.io custom domain route
* fix(kilo-chat): bind NEXTAUTH_SECRET to NEXTAUTH_SECRET_PROD in secrets store
* fix(kiloclaw/kilo-chat): update plugin to send content blocks instead of flat text
Replace `text: string` with `content: ContentBlock[]` in CreateMessageParams and
EditMessageParams, wrap text strings in `[{ type: 'text', text }]` blocks at all
call sites (preview-stream and webhook deliver), and update all three test files
to match. Also change the e2e script SANDBOX_ID default to 'e2e-test-sandbox'.
* fix(kilo-chat): add zod validation at all input boundaries
Convert kiloclaw worker export to WorkerEntrypoint class and add deliverChatWebhook RPC method. kilo-chat now calls kiloclaw directly via service binding instead of external HTTP, eliminating the need for HMAC signing/verification and the KILOCHAT_WEBHOOK_SECRET entirely. - Convert kiloclaw default export from plain object to WorkerEntrypoint - Add deliverChatWebhook RPC: resolves instance from targetBotId, forwards to Fly machine - Enqueue one webhook per bot member (future-proofs multi-bot conversations) - Remove HMAC signing from kilo-chat and verification from plugin - Delete KILOCHAT_WEBHOOK_SECRET from all services, types, and config
Hardcode allowed origins (kilo.ai, app.kilo.ai, localhost:3000) with optional ALLOWED_ORIGINS env var override. Applied to all /v1/* routes. Also fix vitest config to properly stub the kiloclaw service binding.
getBotMembersExcluding returns { id, kind } objects, not strings.
The queue payload needs the string id, not the full object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the kilo-chat channel: a kiloclaw OpenClaw plugin and the external chat backend service it talks to, with Telegram-style live-edit streaming.
kilo-chat plugin (
services/kiloclaw/plugins/kilo-chat/)OpenClaw channel plugin for kiloclaw that talks to the kilo-chat service.
Outbound (four actions via controller proxy):
POST {KILOCHAT_BASE_URL}/v1/messages— create a message; returns{ messageId, version }.PATCH {KILOCHAT_BASE_URL}/v1/messages/:id— edit with monotonicversion;409is treated as a benign drop.DELETE {KILOCHAT_BASE_URL}/v1/messages/:id— preview cleanup on dispatch failure.POST {KILOCHAT_BASE_URL}/v1/conversations/:id/typing— typing indicator (server holds ~5s, plugin re-pings every 3s).All four share the same auth hop: plugin → controller bearer
OPENCLAW_GATEWAY_TOKEN, controller re-auths withBearer KILOCHAT_API_TOKEN+x-kilo-sandbox-idon the upstream leg.Preview streaming: Inbound dispatch instantiates a per-conversation
PreviewStream. FirstonPartialReplytoken POSTs; subsequent partials coalesce into one PATCH per 500ms window.finalizeflushes pending edits, then performs one final PATCH. Dispatch failuresabortand DELETE the in-flight message.Inbound webhook: kilo-chat service delivers webhooks via CF service binding RPC → kiloclaw worker's
deliverChatWebhookmethod → resolves target Fly machine → forwards to controller → OpenClaw SDK dispatch with typing keepalive. No HMAC needed — the service binding is a trusted internal call.Message format: Uses
contentblocks ([{ type: "text", text: "..." }]) for extensibility.kilo-chat service (
services/kilo-chat/)Cloudflare Worker chat backend at
chat.kiloapps.io.Architecture:
ConversationDO(per-conversation state — messages, members, SSE fan-out, typing) andMembershipDO(per-user conversation index with denormalized recency)x-kilo-sandbox-idfor bot/service callersLast-Event-IDreplay, 30s keepalive pings via DO alarmsAPI surface:
POST/v1/conversationsGET/v1/conversationsGET/v1/conversations/:idPOST/v1/messagesGET/v1/conversations/:id/messagesPATCH/v1/messages/:idDELETE/v1/messages/:idGET/v1/conversations/:id/eventsPOST/v1/conversations/:id/typingData model: ULID message IDs (monotonic via
ulidpackage), JSON content blocks, server-controlled versioning for edits (409 on stale), soft deletes. Supports multi-party conversations (data model ready, currently 1:1 user + bot).Wiring
buildEnvVarspassesKILOCHAT_API_TOKEN(encrypted) +KILOCHAT_BASE_URL(plain); secret catalog registersKILOCHAT_API_TOKEN;config-writerenableschannels['kilo-chat']andplugins.entries['kilo-chat']when bothKILOCHAT_API_TOKENandKILOCHAT_BASE_URLare set; Dockerfile builds + installs@kiloclaw/kilo-chatat/usr/local/lib/node_modules/@kiloclaw/kilo-chat; CI content-hash includesplugins/kilo-chat/.Kiloclaw worker converted to
WorkerEntrypointclass to support RPC. kilo-chat service binds to kiloclaw viaservicesbinding in wrangler.jsonc.Test plan
pnpm testinservices/kiloclaw— all passpnpm testinservices/kiloclaw/plugins/kilo-chat— 43 tests (PreviewStream, routes, streaming, typing)pnpm testinservices/kilo-chat— 67 tests (DO unit, route integration, webhook delivery)pnpm typecheckin both services — cleanpnpm lintin both services — 0 errors