@lessinbox/openclaw-channel
v0.2.11
Published
Openclaw channel plugin that sends and receives agent conversation state through Lessinbox.
Readme
@lessinbox/openclaw-channel
Openclaw channel plugin that sends and receives agent conversation state through Lessinbox.
What it does
- Keeps one continuous conversation per target (
channel:*orthread:*) - Starts new Lessinbox sessions (runs) only when needed
- Posts assistant messages into Lessinbox threads
- Maintains persistent
conversationId -> { threadId, runId? }mapping (memoryorredis) - Opens workspace WebSocket stream and emits realtime inbound events (
message.created,interrupt.answered,run.state_changed, etc.) - Exposes a channel plugin manifest + Lessinbox skill bundle
- Exposes guardrails middleware helpers for side-effect tooling (
executeLessinboxGuardedExecution,executeLessinboxOpenclawMiddleware)
Package contents
dist/index.jsplugin runtime entryopenclaw.plugin.jsonmanifestskills/lessinbox/SKILL.md
Required account config
{
"channels": {
"lessinbox": {
"enabled": true,
"toolHooks": {
"enabled": false,
"policyDryRunLogging": true,
"persistExecutionArtifacts": true,
"debugPersistence": false
},
"workItems": {
"autoCreateOnNewConversation": false
},
"accounts": {
"default": {
"enabled": true,
"apiUrl": "https://lessinbox.example.com",
"apiKey": "<LESSINBOX_API_KEY>",
"workspaceId": "wrk_xxx",
"defaultChannelId": "chn_xxx",
"runtimeProviderId": "openclaw",
"inboundCommandAuthorized": false,
"mappingStore": "redis",
"redisUrl": "redis://localhost:6379",
"redisPrefix": "lessinbox:openclaw",
"workspaceStream": {
"enabled": true,
"wsUrl": "wss://lessinbox.example.com/v1/ws",
"reconnectMs": 1500
}
}
}
}
}
}runtimeProviderId defaults to openclaw. Set it to the exact Lessinbox Skill Market provider id for this OpenClaw deployment when you want approvals from any OpenClaw channel (Telegram, Discord, gateway chat, Lessinbox, etc.) to resume through Lessinbox.
inboundCommandAuthorized defaults to false. Set it to true only when your Openclaw policy layer explicitly requires trusted channel-originated command execution.
Tool hook guardrails + approvals (OpenClaw before_tool_call / after_tool_call)
When channels.lessinbox.toolHooks.enabled is true, the plugin registers OpenClaw tool hooks that:
Canonical config path is channel-level (
channels.lessinbox.toolHooks). Backward-compatible fallback is still supported atchannels.lessinbox.accounts.<accountId>.toolHooks.
- run Lessinbox guardrails evaluation for side-effecting tool calls (
tool_call.side_effects) - pause the run when approval is required (via
pauseRun) and optionally invokewaitForInput/resumeRun - emit a policy dry-run log record to the OpenClaw logger (JSON) with keys:
sessionKey,runId,threadId,toolName,argsHash,decision,timestamp - persist dry-run + result artifacts to Lessinbox
/v1/executions(best-effort) so timeline UI shows tool policy checks
If the OpenClaw runtime does not supply sessionKey, pauseRun, or next in the hook context, the plugin falls back to fail-closed evaluation in hook mode:
denyandrequire_approvaloutcomes are blocked.allowoutcomes are passed through to runtime continuation.
This fallback cannot provide deterministic pause/resume lifecycle guarantees by itself; reliable approval resume requires execution-layer support in OpenClaw core.
Set channels.lessinbox.toolHooks.persistExecutionArtifacts=false to keep dry-run logs only (no /v1/executions writes from hooks).
Set channels.lessinbox.toolHooks.debugPersistence=true to emit explicit success/failure debug lines for /v1/executions hook persistence attempts.
Openclaw wiring expectations
The plugin expects Openclaw to call outbound.sendText(...) with:
text(required)config(runtime config object)- optional:
accountId,target,channelId,title,threadId,runId,conversationId,metadata
If threadId and runId are missing, the plugin will create a new Lessinbox run and use that thread.
Target formats supported by the plugin:
channel:<channelId>- continue the latest thread for this channel (or create one if missing)thread:<threadId>- create/continue conversation in that exact thread- raw channel id (
cml...orchn_...) - treated aschannel:<id> - account alias (
default, etc.) - switches account when it matches configured account id
Realtime inbound usage
The package exports subscribeToLessinboxEvents(...) as a direct helper for custom integrations.
Example:
import { subscribeToLessinboxEvents } from "@lessinbox/openclaw-channel"
const unsubscribe = subscribeToLessinboxEvents({
config: runtimeConfig,
accountId: "default",
onEvent: async (event) => {
// event.kind, event.threadId, event.runId, event.conversationId, event.payload
}
})For Openclaw-native runtime lifecycle, inbound processing is wired through channel gateway hooks:
plugin.gateway.startAccount(...)subscribes to Lessinbox workspace stream.plugin.gateway.stopAccount(...)unsubscribes and shuts down account listeners.- Incoming
message.createduser messages are routed through Openclaw runtime reply dispatcher.
This is the preferred runtime path in v1.
On terminal run states (completed, failed, canceled) the plugin keeps thread mapping and clears only the run id, so the next outbound message starts a fresh run in the same thread.
Guardrails middleware usage
Use this helper when Openclaw is about to execute a side-effecting tool action (email, payments, external writes).
import { executeLessinboxGuardedExecution } from "@lessinbox/openclaw-channel"
await executeLessinboxGuardedExecution({
config: runtimeConfig,
accountId: "default",
scope: {
work_item_id: "wi_123",
run_id: "run_123",
thread_id: "thr_123"
},
tool: "email",
action: "send",
request: {
to: "[email protected]",
subject: "Status update"
},
side_effects: {
provider: "smtp"
},
onApprovalRequired: async ({ approval_request_id }) => {
// Optional: wait until someone approves this request in Lessinbox.
console.log("Approval required:", approval_request_id)
},
execute: async () => {
// your real side effect call
return { ok: true }
}
})This enforces Lessinbox policy decisions (deny, require_simulation, require_approval) and writes execution records before/after side effects.
Openclaw pre-tool middleware helper (approval pause/resume)
For Openclaw Option A middleware, use executeLessinboxOpenclawMiddleware(...).
- On first pass, it returns
status: "requires_approval"and does not execute the tool. - After human approval, call again with
approval.approval_request_idto execute safely.
import { executeLessinboxOpenclawMiddleware } from "@lessinbox/openclaw-channel"
const decision = await executeLessinboxOpenclawMiddleware({
config: runtimeConfig,
accountId: "default",
scope: {
work_item_id: "wi_123",
run_id: "run_123",
thread_id: "thr_123"
},
tool_call: {
tool_name: "payments.transfer",
args: { method: "POST", amount: 5000 },
side_effects: { provider: "banking" }
},
execute: async () => {
// real side effect
return { ok: true }
}
})
if (decision.status === "requires_approval") {
// pause Openclaw run and wait for interrupt.answered
}
if (decision.status === "executed") {
console.log("Executed:", decision.result)
}Openclaw tool-runner adapter helper (drop-in)
If you want one adapter function that also calls your Openclaw pauseRun(...) hook, use
executeLessinboxOpenclawToolRunnerAdapter(...).
import { executeLessinboxOpenclawToolRunnerAdapter } from "@lessinbox/openclaw-channel"
const decision = await executeLessinboxOpenclawToolRunnerAdapter({
config: runtimeConfig,
accountId: "default",
context: {
session_key: "sess_123",
work_item_id: "wi_123",
run_id: "run_123",
thread_id: "thr_123"
},
tool_call: {
tool_name: "payments.transfer",
args: { method: "POST", amount: 5000 },
side_effects: { provider: "banking" }
},
pauseRun: async ({ approval_request_id }) => {
// Persist approval id in Openclaw run/session state and pause execution.
await pauseRun({ approval_request_id })
},
execute: async () => {
// real side effect
return { ok: true }
}
})
if (decision.status === "blocked") {
throw new Error(decision.reason)
}
if (decision.status === "paused_for_approval") {
return
}
// decision.status === "executed"
console.log(decision.result)Runtime provider sync (Skill Market)
This channel package does not hardcode Openclaw into Lessinbox. Register Openclaw as a runtime provider and sync its catalog:
POST /v1/runtime-providersPOST /v1/runtime-providers/:providerId/tools/syncPOST /v1/runtime-providers/:providerId/agents/syncGET /v1/runtime-providers/agents?provider_id=:providerId
Use synced runtime_agent_id values in Agent Team definitions (/v1/agent-team/definitions).
Build
pnpm --filter @lessinbox/openclaw-channel buildPublish
pnpm --filter @lessinbox/openclaw-channel build
npm publish --access public