npm package: @lessinbox/openclaw-channel
Description: Openclaw channel plugin that sends and receives agent conversation state through Lessinbox.
Latest version: 0.2.11

# @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:*` or `thread:*`)
- Starts new Lessinbox sessions (runs) only when needed
- Posts assistant messages into Lessinbox threads
- Maintains persistent `conversationId -> { threadId, runId? }` mapping (`memory` or `redis`)
- 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.js` plugin runtime entry
- `openclaw.plugin.json` manifest
- `skills/lessinbox/SKILL.md`

## Required account config

```json
{
  "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 at `channels.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 invoke `waitForInput`/`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:

- `deny` and `require_approval` outcomes are blocked.
- `allow` outcomes 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...` or `chn_...`) - treated as `channel:<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:

```ts
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.created` user 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).

```ts
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: "user@example.com",
    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_id` to execute safely.

```ts
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(...)`.

```ts
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-providers`
- `POST /v1/runtime-providers/:providerId/tools/sync`
- `POST /v1/runtime-providers/:providerId/agents/sync`
- `GET /v1/runtime-providers/agents?provider_id=:providerId`

Use synced `runtime_agent_id` values in Agent Team definitions (`/v1/agent-team/definitions`).

## Build

```bash
pnpm --filter @lessinbox/openclaw-channel build
```

## Publish

```bash
pnpm --filter @lessinbox/openclaw-channel build
npm publish --access public
```