Skip to content
dsh.fish
Bundle

dsh-ringcentral

RingCentral Team Messaging IM channel plugin for deepseek-harness (dsh)

Source
ringclaw
stars
1 stars
License
MIT
Updated
Updated 5 days ago

Readme

# dsh-ringcentral

RingCentral Team Messaging IM channel plugin for
[deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
It turns a RingCentral Bot Add-in into a first-class frontend for a dsh agent:
inbound posts from RingCentral chats drive the agent loop, and assistant
replies flow back as threaded RingCentral posts.

English | [中文说明](./README.zh-CN.md)

## Architecture

```
RingCentral user ──▶ WebSocket (PostAdded) ──▶ im-ringcentral ──▶ ctx.agents ──▶ dsh agent loop ──▶ LLM
                                                │                                     │
                                                └── admission / session / event ◀────┘
                                                     (assistant reply ──▶ RingCentral post, threaded)
```

The plugin is a pure Cordis plugin following the dsh "Plugins, not loop changes"
principle. It speaks the RingCentral Team Messaging v1 REST API + WebSocket
subscription stream directly (no external SDK) and reuses the host dsh services
for agents, sessions, models, compaction, and tool presentation.

## Install

### 1. Via dsh plugin manager

```bash
# install into a profile
npx @deepseek-ai/dsh plugin --profile ringcentral add dsh-ringcentral

# start
export RC_BOT_TOKEN="your-bot-jwt"
export DEEPSEEK_API_KEY="your-deepseek-key"
npx @deepseek-ai/dsh --profile ringcentral
```

Or run the bundled installer: `sh install.sh`.

### 2. Local path

```bash
cd /path/to/dsh-ringcentral
pnpm install && pnpm build
npx @deepseek-ai/dsh plugin --profile ringcentral add /path/to/dsh-ringcentral
export RC_BOT_TOKEN="your-bot-jwt"
npx @deepseek-ai/dsh --profile ringcentral
```

### 3. --patch development mode

The `--patch` overlay loads the plugin from a local absolute path without
installing it into a profile. Generate the machine-local patch first, then
boot:

```bash
cd /path/to/dsh-ringcentral
pnpm install && pnpm build        # dist entry (npx dsh cannot resolve .js -> .ts)
node scripts/gen-dev-patch.mjs    # writes cordis.local.yml with the real path
export RC_BOT_TOKEN="your-bot-jwt"
npx @deepseek-ai/dsh web --patch ./cordis.local.yml
```

Use `pnpm dev` (tsc --watch) while iterating: the loader hot-reloads the
plugin whenever `dist/` changes. Pointing the patch at `src/index.ts` only
works inside a deepseek-harness source tree (`pnpm dsh`), not with the
`npx`-installed package.

## RingCentral bot setup

1. Sign in at <https://developers.ringcentral.com/>.
2. Create an app with the **Bot** platform type.
3. Grant at least: `TeamMessaging`, `ReadAccounts`, `WebSocketsSubscription`.
4. Install or publish the bot to your RingCentral account.
5. Copy the bot JWT and use it as `RC_BOT_TOKEN`.

Optional owner credentials (JWT REST API app for your own account, with
`TeamMessaging` + `WebSocketsSubscription` + `ReadMessages`) give
`ringcentral_get_recent_messages` owner read fallback and outbound owner
fallback. The history tool always reads through the bot client
(`RC_BOT_TOKEN`) first; without owner credentials it uses only the bot
client (chats the bot is a member of).

## Configuration

Config follows dsh practice: **the cordis config tree is the single source**
(profile `cordis.patch.yml` / `cordis.yml`), with Schema defaults applied
automatically. Secrets (`RC_BOT_TOKEN`, `RC_USER_CLIENT_ID`,
`RC_USER_CLIENT_SECRET`, `RC_USER_JWT_TOKEN`) resolve through the host
**credentials** domain — an explicit config value wins, then environment →
managed `$DSH_HOME/.credentials.yaml` → project/user `.env`, with the plain
process environment as the final fallback. `RC_SERVER_URL` works the other
way for operational overrides (e.g. sandbox): the environment value wins over
the configured/default server. Secrets are never persisted into profile YAML.
To drive any other setting from an environment variable, use the cordis
loader's `!!js` tag (double bang — a single `!js` is not evaluated), e.g.
`access.groupMode: !!js process.env.RC_GROUP_MODE ?? 'open'`.

The access-control block mirrors `@tencent-connect/dsh-qqbot` exactly
(QQ's `c2c` surface is `dm` here). RingCentral's three non-DM chat types
(Team / Everyone / Group) are all governed by the `group` surface.

| Config | Type | Default | Description |
| --- | --- | --- | --- |
| `botToken` | string | **required** | Bot static JWT (env: `RC_BOT_TOKEN`) |
| `ownerCredentials.clientId` / `clientSecret` / `jwt` | string | - | Owner JWT (env: `RC_USER_*`) |
| `server` | string | `https://platform.ringcentral.com` | API server (env: `RC_SERVER_URL`) |
| `access.dmMode` | enum | `open` | DM handling: `disabled`, `allowlist`, `open` |
| `access.dmAllow` | string[] | `[]` | Person ids allowed in DMs; empty or `["*"]` = allow all |
| `access.groupMode` | enum | `open` | Group handling: `disabled`, `allowlist`, `open` |
| `access.groupAllow` | string[] | `[]` | Chat ids allowed in groups; empty or `["*"]` = allow all |
| `requireMention` | boolean | `true` | Require `@`-mention in group chats |
| `groupPrompt` | string | - | Extra system prompt for group chats |
| `directPrompt` | string | - | Extra system prompt for DMs |
| `processingPlaceholder.enabled` | boolean | `false` | Post `👀` → `⏳` while the agent works (texts/delay are fixed) |
| `historyMessageLimit` | number | `250` | Default record count for the history tool |
| `homeChannel` | string | - | Fallback target for the history tool |
| `textChunkLimit` | number | `4000` | Max chars per outgoing post |
| `provider` / `model` | string | host default | LLM route (fallback chain: per-peer prefs → config → host `agentDefaultModel`) |
| `preset` | string | - | Agent preset id |
| `cwd` | string | `process.cwd()` | Agent working directory |
| `sessionIdleTimeout` | number | `1800000` | Idle session eviction (ms) |
| `showToolResults` | boolean | `false` | Show successful tool results (errors always show) |
| `debug` | boolean | `false` | Debug logging (includes inbound message logs) |

Every row except the secret entries is editable from the Web GUI card
described below.

## Web GUI configuration

Installed into a web profile (`dsh plugin --profile web add dsh-ringcentral`),
the plugin registers a settings namespace and a configuration card under
**Settings → Plugins → Plugin configuration**. The card:

- edits every non-secret entry of the table above; saves apply to the **next
  inbound IM message** (no restart), and a per-field **Reset** returns a value
  to the composed profile layer;
- renders the four secrets (`RC_BOT_TOKEN`, `RC_USER_*`) as **write-only**
  controls: it reports only whether each is configured and writes through the
  settings domain (fallback to the host credentials domain where reachable);
  secret values are never sent back to the page. Credential changes apply
  **live**: within ~1s the plugin rebuilds its REST clients and reconnects the
  websocket with the new token — no restart, and the per-peer session context
  survives rotation (the account key derives from the stable bot identity).

Without a settings service (a bare custom `cordis.yml`), the card simply does
not mount and the plugin behaves exactly as before.

## Commands

| Command | Description |
| --- | --- |
| `/new` (`/reset`, `/clear`) | Start a new session (clear context) |
| `/compact` | Compress session history (summary replaces old records) |
| `/model` | Show or switch the model |
| `/stop` | Abort the current generation |
| `/rc-ping` | Connectivity test |
| `/rc-version` | Plugin version |
| `/rc-status` | Current session status |
| `/rc-help` | List all commands |

## Session routing

`sessionKey: ringcentral:<accountScopeKey>:<scope>:<peerId>` where scope is
`direct` (peer = person id), `group` (peer = Group DM chat id), or
`channel` (peer = Team/Everyone chat id), and `accountScopeKey` is a
SHA-256 fingerprint of server + bot token. The `SessionId` is derived
deterministically (SHA-256), so the same user/chat always routes to the same
session and survives restarts. Resolution order: in-process reuse → persisted
resume → fresh create.

## Agent questions (ask_user)

When the agent calls `ask_user_question`, the plugin renders the question
into the chat (thread-anchored) and waits for the user to reply in the same
session:

- Reply with an option number or label to select a choice (`multi_select`
  accepts `"1, 3"`), or type a free-text answer.
- Multi-question asks are answered one question at a time.
- Answers resolve the pending question and are **not** appended to session
  history (same semantics as the web GUI).
- Waiting times out after 10 minutes (the ask is cancelled with a notice).

Note: the provider registers on the `userQuestions` service seam. In a web
profile the GUI provider takes precedence — questions then appear in the web
UI, not in RingCentral. Use a dedicated profile for IM-only operation.

## Design principles

- **Pure Cordis plugin** — follows dsh "Plugins, not loop changes".
- **Declarative deps** — `inject = ['agents']`; tools/compaction/presets are optional seams.
- **Session isolation** — one agent per RingCentral peer.
- **Mini-Markdown outbound** — replies are converted to RingCentral Mini-Markdown and chunked.
- **Threading** — replies always anchor on the triggering post (threadId preferred), with owner fallback and unthreaded retry.
- **Idle eviction** — inactive agents are disposed automatically.
- **Defensive degradation** — missing tools/presets/owner credentials never crash the plugin.

## Not in v1 (planned follow-ups)

- Adaptive Card / note / calendar / task artifact tools
- Cron and out-of-process notification sender
- Multi-account support
- Native streaming (RingCentral has no stream API; the processing placeholder is the typing affordance)

## Local development

```bash
pnpm install
pnpm build          # or: pnpm dev (watch)
pnpm test
pnpm typecheck

# run against the npx-installed dsh
export RC_BOT_TOKEN="xxx"
node scripts/gen-dev-patch.mjs
npx @deepseek-ai/dsh web --patch ./cordis.local.yml
```

`scripts/gen-dev-patch.mjs` emits the gitignored `cordis.local.yml` directly,
with the entry resolved to an absolute path on the machine (default
`dist/index.js`; pass `src/index.ts` to point at the TypeScript entry).

## Test coverage map

`pnpm test` runs 143 cases across 17 files. No network is touched — clients,
websockets, and harness services are stubbed.

| File | Covers |
| --- | --- |
| `tests/accounts.test.ts` | Secret resolution precedence, schema-default fallbacks, legacy `__FROM_ENV__` compat, numeric clamping |
| `tests/account-scope-key.test.ts` | Stable-identity account key: rotation keeps session keys; token-fingerprint fallback |
| `tests/credentials.test.ts` | `resolveSecret` chain (service → env → managed file), inject handle, file parser, file watcher |
| `tests/settings-merge.test.ts` | Live config merge into runtime/account copies, secret-field exemption, server/placeholder branches |
| `tests/settings-redact.test.ts` | `role('secret')` wire redaction and its sidecar |
| `tests/settings-section.test.ts` | Cross-version settings mounting: `installSection` (harness ≥ 0.1.2) vs legacy `installSettingsSection` fallback (≤ 0.1.1-rc.2), failure containment |
| `tests/monitor.test.ts` | WS frame extraction, post filtering (own echoes, answer markers), websocket URL building |
| `tests/rotation-scheduler.test.ts` | Debounce coalescing, single-flight, dispose (extracted module, fake timers) |
| `tests/gateway-rotation.test.ts` | **Fake-harness integration**: `rotate()` rebuilds clients, re-registers the history tool, reconnects the websocket; no-op on same token; fail-safe on missing token |
| `tests/session-manager.test.ts` | Deterministic session ids, per-scope isolation, `setAccountKey` re-derivation |
| `tests/inbound.test.ts` | Admission (dm/group allowlists), mention gating, prompt injection, command routing |
| `tests/outbound.test.ts` | Reply chunking, `showToolResults` gating, placeholder lifecycle |
| `tests/history-tool.test.ts` | Reader precedence (bot → owner), record clamping, target resolution |
| `tests/threading.test.ts` | Thread-participation tracking and follow-up classification |
| `tests/targets.test.ts` | Reply-target resolution and thread anchoring |
| `tests/markdown.test.ts` | Mini-Markdown conversion |
| `tests/user-questions.test.ts` | IM answer surface for agent questions |
| `tests/debug-log.test.ts` | On-disk debug log format and gating |

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Plugin not starting | `RC_BOT_TOKEN` missing | Set `RC_BOT_TOKEN` (env or `$DSH_HOME/.credentials.yaml`) or `botToken` in config |
| Debug log shows `[cred] … service=absent` | In desktop-style deployments, bundle-row plugins cannot read host services via `ctx.get` (isolate scoping) | Not an error: 0.3.15+ ships a file fallback and 0.3.17 an inject-based path — see [docs/credentials-service-unreachable.md](docs/credentials-service-unreachable.md) |
| Bot never replies in a group chat | `access.groupMode: disabled`, not allowlisted, or no mention | Check `access.groupMode` / `access.groupAllow` and `@`-mention the bot |
| DM ignored | `access.dmMode: disabled` or sender not in `access.dmAllow` | Check `access.dmMode` / `access.dmAllow` |
| History tool returns nothing | Chat not visible to bot or owner | Reads try the bot first, then the owner; pass a bare chat id or `channel:<chatId>` and make sure one client is a member |
| Agent question not answered from RingCentral | Web GUI provider registered (web profile) | Questions go to the web UI; use a dedicated profile or answer in the web UI |
| Plugin config card missing from the Web GUI, or save/reset fails, after a harness upgrade | Harness ≥ 0.1.2 moved settings registration to `settings.installSection()` and removed `connection.api`; ≤ 0.4.1 used the removed APIs | Upgrade to `dsh-ringcentral` ≥ 0.4.2 (dual-path: `installSection` on ≥ 0.1.2, legacy fallback on ≤ 0.1.1-rc.2) |

## Harness compatibility

- `dsh-ringcentral` ≥ 0.4.2 supports both harness lines: **≥ 0.1.2-alpha** (settings via `settings.installSection`, writes via `settingsScope.mutate`) and **0.1.1-rc.2 and earlier** (legacy `installSettingsSection` + `connection.api.settings.mutate` fallback).
- `dsh-ringcentral` ≤ 0.4.1 requires the ≤ 0.1.1-rc.2 settings API; it does not start on harness ≥ 0.1.2.

## License

[MIT](./LICENSE)

Install

dsh plugin --profile web add github:ringclaw/dsh-ringcentral#827a1b9eadb228062b4df2bf7a6a919c3a9127e9

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
Source