Skip to main content
Glama
openmaxai

claude-openmax

by openmaxai
README.md
# claude-openmax

The **Claude Code** runtime adapter for OpenMax / CWS. A thin **Category-B**
(bare runtime) adapter: it owns none of the CWS protocol — that all comes from
[`@openmaxai/openmax-agent-sdk`](../cws-agent-sdk) (`CwsAgentBridge`) — and does
only the two runtime-specific translations plus capability exposure:

1. **Inbound** — bring a workspace message into Claude Code's *visible context*
   via an experimental `claude/channel` MCP push (`raft-channel-wake.v1`).
2. **Outbound** — send Claude's reply back to cws-core via the SDK's
   `CommService`.
3. **Capability exposure** — the SDK's six service clients
   (`tm`/`kb`/`as`/`comm`/`core`/`conn`) as MCP tools, so the agent can operate
   the workspace (create issues/tasks, query the KB, upload files, reply).

It follows the proven reference implementation,
[`raft-external-agents` v0.3.1](https://github.com/botiverse/raft-external-agents)
— the only one of the four external runtimes with a shipped Claude Code plugin —
and the [claude-openmax adapter design](#).

## Architecture

```
Layer 1  @openmaxai/openmax-agent-sdk  (CWS HTTP/WS contract only)
  CwsAgentBridge: per-org WS lifecycle · auth/heartbeat/reconnect · atomic dedupe
    · /sync + inbox-ledger · frame dispatch · access-policy · normalized InboundMessage
  services: tm / kb / as / comm / core / conn   (one CwsHttpClient)
  providers: StorageProvider · RuntimeStateProvider · InboundDelivery(★) · Logger
        ▲ import + inject
Layer 2  claude-openmax  (this repo)
  ┌ bridge host (Node) ────────────────┐        ┌ Claude Code (agent) ──────────┐
  │ new CwsAgentBridge({providers,cbs}) │        │ MCP `openmax` server:         │
  │  providers.inbound.deliver ─────────┼─wake──▶│  experimental claude/channel  │
  │   = derive WakeRequest → push       │        │  → pushes notice into context │
  │  storage=local data dir · logger    │        │ MCP tools: tm kb as comm core │
  │  holds 6 SDK service clients ◀──────┼─call───┤  conn + comm_send             │
  └─────────────────────────────────────┘        └───────────────────────────────┘
        │ CommService.send() / bridge.send() → cws-core
        ▼
     cws-core REST  ◀── cws-comm WS (inbound frames) ── COCO Workspace (user)
```

### Topologies

- **In-process (MVP, default)** — one Node process is both the stdio MCP server
  (`claude/channel` + tools) **and** the host of `CwsAgentBridge`. `InboundDelivery`
  pushes wakes straight to the channel; the `/wake` HTTP hop is skipped but the
  `WakeRequest` wire shape is preserved. Run: Claude Code loads the plugin.
- **Split (design topology 1, `CLAUDE_OPENMAX_MODE=channel-only`)** — the MCP
  plugin runs only the channel + an HTTP `POST /wake` server; a **separate**
  resident `bridge.js` holds the WS and POSTs wakes. The bridge survives Claude
  Code session restarts and redelivers via the SDK's `/sync` + inbox-ledger.

## The `ok:true` delivery invariant

The single most important rule (from the SDK's `wake-result` schema and
`CwsAgentBridge`): **`ok:true` MUST mean the message genuinely entered the
runtime's visible context.** On `ok:true` the SDK commits dedupe + ledger +
read markers and *stops* `/sync` retry for that message — so a false `ok:true`
loses the message forever.

This adapter returns `ok:true` **only** when the wake injection resolved
(`ClaudeChannel.notifyWake` / `POST /wake` succeeded). Anything else —
channel not connected, notification write failed, malformed inbound — returns
`{ok:false, failureClass, retryAfterMs}`, so the SDK holds all markers and
redelivers on the next `/sync` sweep. See `src/inbound-delivery.js` and its
tests.

## Files

| File | Purpose |
|------|---------|
| `src/index.js` | MCP channel plugin entrypoint (Claude Code loads this over stdio); default in-process bridge host. |
| `src/bridge.js` | Standalone resident bridge for the split topology; POSTs wakes over HTTP `/wake`. |
| `src/channel.js` | MCP `Server` declaring the experimental `claude/channel` capability; `notifyWake` pushes `notifications/claude/channel`. |
| `src/wake.js` | Pure `raft-channel-wake.v1` derivation + validation + the human-visible wake notice/meta builders. |
| `src/inbound-delivery.js` | `InboundDelivery.deliver()` — derive WakeRequest, inject, gate `ok:true`. |
| `src/notifier.js` | Debounced wake coalescing (raft EAB-8): leading-edge inject + window merge. |
| `src/wake-server.js` | HTTP `POST /wake` server for the split topology (token-guarded). |
| `src/mcp-tools.js` | Wraps the six SDK service clients as MCP tools (one dispatch tool per service + `comm_send`). |
| `src/config.js` | Loads adapter config; builds `CwsHttpClient` + `TokenManager` + services; SDK callback seams (session/config/owner persistence). |
| `src/create-bridge.js` | Assembles `CwsAgentBridge` from the runtime + providers. |
| `src/storage.js` | File-backed `StorageProvider` under a local data dir (XDG); no `~/zylos` coupling. |
| `src/providers.js` | stderr logger + empty `RuntimeStateProvider` (Cat.B degraded metrics). |
| `.claude-plugin/plugin.json` | Registers the `openmax` MCP server for Claude Code. |
| `hooks/hooks.json` + `hooks/session-hook.js` + `hooks/orientation.js` | `SessionStart` orientation injection (survives resume/compaction). |
| `CLAUDE.md` | Agent-facing instructions: how wakes arrive, how to read/reply, tool map. |
| `test/*.test.js` | `node --test` unit tests (frame derivation, `ok:true` gating, coalescing, tool dispatch, orientation). |

## Session / context management

Uses Claude Code's **built-in autocompact** (and `/clear` / `/compact`). This
adapter implements **no extra compression logic** — by design.

## Installation

Install as a Claude Code plugin from this repo's marketplace:

```bash
claude plugin marketplace add openmaxai/claude-openmax   # register the marketplace (once)
claude plugin install openmax-channel@openmax            # install the plugin
```

Claude Code fetches the plugin, installs it into its managed plugin directory,
and keeps it updated — no manual `git clone` needed. Then create your config
(see [Configuration](#config--env)) and start Claude Code.

> **Experimental wake — one caveat.** The MCP **tools** (`tm`/`kb`/`as`/`comm`/
> `core`/`conn` + `comm_send`) work from a plain plugin install with no flags.
> The **inbound wake** (`claude/channel`, how workspace messages reach the agent)
> is still an *experimental* Claude Code capability and currently requires
> launching with the development-channels flag:
>
> ```bash
> claude --dangerously-load-development-channels plugin:openmax-channel@openmax
> ```
>
> **Reference the plugin, not a bare server name.** When installed via
> `claude plugin install`, Claude Code namespaces this MCP server — `/mcp` shows
> it as `plugin:openmax-channel:openmax`, **not** `openmax`. So the bare
> `--dangerously-load-development-channels server:openmax` does **not** match a
> plugin install (it only works if you register the server directly in a
> `.mcp.json` under the literal name `openmax`); use the `plugin:…@…` form above.
> If in doubt, run `/mcp` and reference the exact server name it prints.
> Once `claude/channel` graduates from experimental, install alone will be enough.

## Running (from source / dev)

```bash
npm install                     # resolves @openmaxai/openmax-agent-sdk@alpha from npm
cp config.example.json ~/.config/claude-openmax/config.json   # fill in real values
npm test                        # node --test
```

Load into Claude Code as a plugin (dev):

```bash
claude plugin marketplace add --scope local /path/to/claude-openmax   # local checkout
# or point Claude Code at .claude-plugin/plugin.json directly
```

## Build (maintainers)

The plugin ships a **dependency-free bundle**. Claude Code installs a marketplace
plugin by cloning the repo and does **not** run `npm install`, so the MCP server
must run with zero `node_modules`. `scripts/build.js` (esbuild) inlines every
dependency into `dist/index.mjs` (the MCP server, referenced by
`.claude-plugin/plugin.json`) and `dist/bridge.mjs` (the split-topology bridge).

```bash
npm run build     # rebuild dist/ after changing src/ or bumping a dependency
```

`dist/` is committed (it is the shipped artifact); CI rebuilds it and fails if the
committed bundle is stale, and smoke-tests that it loads with no `node_modules`.

Split topology (resident bridge + channel-only plugin):

```bash
# terminal A: Claude Code loads the plugin with
CLAUDE_OPENMAX_MODE=channel-only CLAUDE_OPENMAX_WAKE_PORT=47600 CLAUDE_OPENMAX_WAKE_TOKEN=... claude ...
# terminal B: resident bridge (config.wake.endpoint = http://127.0.0.1:47600/wake)
CLAUDE_OPENMAX_WAKE_TOKEN=... node src/bridge.js
```

### Config / env

Config file at `$CLAUDE_OPENMAX_CONFIG` (or `~/.config/claude-openmax/config.json`);
see `config.example.json`. As of the config-parity refactor the on-disk shape is a
**1:1 structural mirror of the OpenMax (`zylos-openmax`) component's config** — see
the migration note below. The shape:

```
enabled?: bool
server:  { bff_url, ws_url, frontend_base_path }        // frontend_base_path default "/workspace"
agent:   { identity_id, api_key, device_id, app_version }
cf_access: { client_id, client_secret }
orgs:    { "<org_id>": { enabled?, org_id, org_name?,
             owner: { member_id, name },
             self:  { member_id, name, display_name },
             access:{ dmPolicy, dmAllowFrom?, groupPolicy?, groups?:{ "<convId>": { mode, allowFrom } } } } }
wake:    { endpoint }                                   // claude-openmax ONLY (openmax has no wake)
metricsReport?: { dashboardApiKey }                     // RESERVED / forward-compat — inert (no reporter yet)
ws?:     { reconnectMaxMs?, heartbeatIntervalMs?, pingIntervalMs? }   // claude-openmax WS tuning knobs
```

Env fallbacks (map onto the nested fields): `COCO_API_URL`→`server.bff_url`,
`COCO_WS_URL`→`server.ws_url`, `COCO_FRONTEND_BASE_PATH`→`server.frontend_base_path`,
`COCO_API_KEY`→`agent.api_key`, `COCO_DEVICE_ID`→`agent.device_id`,
`COCO_CLIENT_VERSION`→`agent.app_version`, `COCO_ORG_ID`→default org. Other knobs:
`CLAUDE_OPENMAX_DATA_DIR`, `CLAUDE_OPENMAX_MODE`, `CLAUDE_OPENMAX_DEBOUNCE_MS`,
`CLAUDE_OPENMAX_CONTENT_FREE`, `CLAUDE_OPENMAX_WAKE_{HOST,PORT,TOKEN}`.

**`orgs` is keyed by `org_id`** (openmax-style), end to end: the SDK orchestrator
keys its per-org runtime records by `org_id` too, so the adapter hands it an
`org_id`-keyed map directly — there is no separate per-org key to derive. Every
self-healing write-back (`self.member_id`, `self.name`, owner bind) resolves the
org by `org_id` and lands back in the `org_id`-keyed on-disk structure.

**`agent.identity_id`** is the agent's global identity. Leave it empty and the
adapter resolves it from cws-core `GET /me` at startup and caches it back to
`config.json`. It is the `leadAgentId` for the guided-autonomy flow (an Issue's
Lead agent = the agent itself).

**`server.frontend_base_path`** is wired into the SDK's `CwsHttpClient.frontendUrl()`
so the agent can build clickable workspace links (`<bff_url><frontend_base_path>/…`,
default `/workspace`).

### Migrating from the openmax (`zylos-openmax`) component

The claude-openmax config is now **structurally identical** to the openmax
component's `config.json` — you can drop an openmax config in as-is. The only
differences are additive and claude-openmax-specific:

- **`wake.endpoint`** — required for the split-topology bridge; openmax has no wake block.
- **`metricsReport`** — accepted for parity but **inert** (claude-openmax has no
  metrics reporter yet); it round-trips untouched.
- **`ws`** — optional WS tuning knobs (`reconnectMaxMs`, `heartbeatIntervalMs`,
  `pingIntervalMs`) that openmax hardcodes; `ws_url`/`device_id`/`app_version` live
  under `server.*`/`agent.*`, NOT here.

The **old** claude-openmax shape (top-level `http`/`auth` + an **array** `orgs`) is
still accepted: it is translated to the new shape on load with a one-time warning,
so an existing live config won't break — but you should migrate it.

**Session files auto-migrate.** Per-org state (incl. the `/sync` cursor) is now
keyed by `org_id` (`sessions/<org_id>.json`), where earlier builds used a derived
slug (`sessions/<slug>.json`). On first load, if only a legacy `sessions/<slug>.json`
exists (matching an explicit `slug` or `slugify(org_name)`), it is copied forward
to the `org_id` key so the cursor is preserved — no duplicate message delivery after
upgrade. The old file is left in place (harmless); no manual step is needed.

Each org also honors an **`enabled: false`** flag — such orgs are kept on disk but
not connected to (parity with the openmax component).

## Verified vs. spike (honesty ledger)

**Verified locally (`node --test` + MCP client smoke):**
- WakeRequest derivation/validation, `ok:true` gating, coalescing, tool dispatch — 32 unit tests green.
- The MCP server boots, advertises `capabilities.experimental["claude/channel"]`, and lists all 7 tools to a real MCP client.
- `POST /wake` (token-guarded) → `server.notification({method:"notifications/claude/channel", ...})` is **actually transmitted over the MCP transport and received by the connected client** with correct content + routing meta, and the server returns `{ok:true, runtimeSession}`. `server.notification` with the custom `claude/channel` method does **not** throw on `@modelcontextprotocol/sdk` 1.22+.

**⚠️ SPIKE — requires a live Claude Code to confirm (biggest technical uncertainty):**
1. **`claude/channel` rendering.** We proved the notification reaches an MCP
   *client*; we have **not** proved Claude Code (a) enables this experimental
   capability, (b) renders the pushed notice into the agent's *visible context*,
   and (c) does so promptly / can steer an in-progress turn. Until confirmed,
   `ok:true` means "notification written to the MCP transport", which is the
   strongest local signal but weaker than "the model has seen it". If Claude
   Code proves fire-and-forget here, we should fall back to a more conservative
   `ok:true` gate (and lean harder on `/sync`).
2. **`runtimeSession` binding.** We mint a stable per-process id; the canonical
   value is Claude Code's real session id, which must be sourced from the live
   runtime.
3. **Session lifecycle / auth jitter** under the in-process topology (WS drops
   when the Claude Code session exits) needs a live soak test; the split
   topology is the mitigation and also needs end-to-end restart verification.
4. **Tool budget.** We collapsed ~150 sub-commands into 6 dispatch tools +
   `comm_send` to stay within Claude Code's tool budget; the exact budget and
   whether dispatch-style tools are ergonomic for the model is unconfirmed.

## Boundaries

- Consumes the SDK for **all** protocol/transport/sync/dedup/access-policy logic;
  reimplements none of it. Does **not** pass a custom `callbacks.dedupe` (uses the
  SDK's built-in atomic deduper).
- No dependency on `zylos-openmax`; independent local data dir (no `~/zylos`).

TDQS

B3/5.0

Scored across 7 tools

Disambiguation2/5

Tools have overlapping purposes: comm and comm_send both handle sending messages, and core's projectList duplicates tm's projectList. This creates ambiguity for an agent.

Naming Consistency2/5

Tool names are inconsistent: some are short abbreviations (as, tm), others are full words (comm, core), and one uses an underscore (comm_send). No clear pattern.

Tool Count4/5

7 tools is reasonable for the breadth of services, though some consolidation could reduce overlap. Still within an acceptable range.

Completeness3/5

Covers major domains (artifacts, tasks, comm, KB, identity, connections) but has noticeable gaps like missing update/delete for many entities and a separate send tool that feels incomplete.

Maintenance

ActivityMaintained
ResponsivenessNo issues