Tools
Alter Plugin Oss
OpenClaw plugin for Alter Vault — agent OAuth/credential access
Install
npm install
pnpm
README
# @alter-ai/openclaw-plugin
Multi-user identity, OAuth grants, HITL approvals, and per-user audit for [OpenClaw](https://github.com/openclaw/openclaw) deployments — backed by [Alter Vault](https://alterauth.com).
Turn one OpenClaw deployment into a **team-hosted, multi-user agent**: each team
member signs in once via your IdP, links their channel identity (Slack /
WebChat / Telegram / etc.) to an Alter `app_user`, connects their own provider
accounts via Alter Connect, and the OpenClaw agent acts on their behalf with
HITL gating and per-user audit. Each user opens Alter Wallet to see *their*
audit log, grants, and pending approvals — not anyone else's.
> **Status:** v0.0.1 — early. The security primitives (AAD-bound at-rest
> encryption, App SDK chokepoint, HMAC-signed Connect state with grant-id
> ownership check, identity-collision rejection, JWT lifetime enforcement) are
> built and tested; the operator setup flow works end-to-end with Clerk.
## Install
```sh
# in your OpenClaw deployment dir
openclaw plugins install @alter-ai/openclaw-plugin
# or local checkout:
openclaw plugins install ./openclaw-alter-plugin
```
Requires Node ≥ 22 (uses the `node:sqlite` built-in). On boot, you'll see
`ExperimentalWarning: SQLite is an experimental feature` — that's Node telling
you `node:sqlite` is still flagged experimental in 22-24. It works, and the
warning will go away when Node graduates the API.
See **[OPERATOR_SETUP.md](./OPERATOR_SETUP.md)** for the full provisioning
recipe (Alter app registration, Clerk JWT template, env vars, Google OAuth,
Tavily, etc.). Reading that doc end-to-end is the only way to actually run
this plugin.
## What it gives users
After install, every user in your channel gets:
- `/alter login` — link their channel identity to an Alter account.
- `/alter connect <provider>` — connect a provider (e.g. Google) for OAuth.
- `/alter connections [revoke <id>]` — list / revoke their grants.
- `/alter wallet` — open the Alter Wallet (per-user audit log + grants).
- `/alter status [clear]` — show link state + approval lock; clear if stale.
And these tools, addressable by the LLM in any chat turn:
| Tool | Provider | HITL | Description |
|---|---|---|---|
| `alter_gmail_search` | google | no | Search the user's Gmail |
| `alter_gmail_send` | google | yes | Send email as the user |
| `alter_gcal_list_events` | google | no | List calendar events |
| `alter_gcal_create_event` | google | yes | Create a calendar event |
| `alter_web_search` | tavily | no | Tavily web search |
Adding a new tool is a 30-line file using `registerAlterTool` — see
`src/tools/web-search.ts` for the smallest example. Anything Alter has a
provider for can be exposed this way.
## Architecture in one paragraph
`src/index.ts` exports a `definePluginEntry({register})`. The `register`
callback installs HTTP routes (`/alter/link*`, `/alter/connect/cb`), slash
commands, the `llm_output` rate-limit hook, an operator CLI surface
(`openclaw alter db-rotate-key|rotate-state-hmac|unlink-channel-user`), and
one tool factory per tool. Each factory closes over
`ctx.{sessionId, messageChannel, requesterSenderId}` (trusted, runtime-provided
by OpenClaw) and returns an `AnyAgentTool`. The tool's `execute()` runs the
identity gate against `(messageChannel, requesterSenderId)` — looking up an
encrypted JWT in SQLite, checking the per-day rate-limit, finding the right
grant, then calling Alter via `agent.proxyRequest({grantId, …})`. HITL prompts
return both an `isError:true` hint and set a per-session `pendingApprovalLock`
that blocks subsequent tool calls until cleared.
## Security guarantees
Each is implemented and unit-tested in `tests/`:
- **At-rest encryption with AAD binding.** Identity JWTs are stored
AES-256-GCM encrypted with `(channel_kind, channel_user_id, row_id)` bound
into the AAD. A SQLite-write attacker who swaps two ciphertext rows fails
GCM authentication on the next read. (`tests/crypto.test.ts`.)
- **App SDK chokepoint.** The `App` instance is wrapped in a Proxy that
refuses `listGrants` / `revokeGrant` / `revokeDelegation` whenever no user
JWT is in `currentUserJwt` AsyncLocalStorage scope. A stray refactor that
forgot `currentUserJwt.run(jwt, …)` throws loudly instead of silently
cross-user-listing.
- **HMAC-signed Connect state with grant ownership check.** `/alter connect`
signs an HMAC payload binding `(csrfNonce, sessionId, channelKind,
channelUserId, appUserId, provider, iat, ttl)`. The callback verifies the
HMAC, consumes a single-use csrfNonce row, AND verifies the
newly-created `grant_id` actually belongs to the bound `appUserId` via
`app.listGrants({since: state.iat, providerId})` before persisting.
- **Identity-collision rejection.** Re-link with the same IdP `sub` updates
the JWT (last-write-wins is fine within an identity); re-link with a
*different* `sub` for the same `(channel_kind, channel_user_id)` is
REJECTED rather than silently re-pointing grants. (`tests/identity-store.test.ts`.)
- **JWT lifetime enforcement.** Link callbacks reject JWTs whose
`exp - iat < 24h` so a misconfigured IdP template can't degrade the gate to
"re-sign-in every 60 seconds."
- **JWKS cache invalidate-on-kid-miss.** When the IdP rotates signing keys,
the first miss triggers a JWKS refetch + retry instead of fail-closing for
the rest of the cache hour.
- **Belt-and-suspenders user verification.** The link callback verifies the
JWT locally with `jose` AND with `app.verifyUserToken(jwt)` (Alter's
server-side verifier). Both must agree on the `sub`.
- **Byte-aware tool-result truncation.** `capToolResultJson` measures UTF-8
bytes (not UTF-16 code units), caps lists, and always emits parseable JSON
with a `{truncated, truncatedReason, partial}` envelope.
### Known security caveats
Read these before pointing the plugin at real users:
- **HITL is enforced as a _post_-check, not a pre-check.** The Alter SDK does
not expose a grant's `requires_approval` policy on `OAuthGrantItem`, so the
plugin cannot block a write *before* it executes. For an `hitl: true` tool it
verifies *after* the call that `proxyRequest` returned a `PendingApproval`; if
the server-side policy is missing `requires_approval`, the irreversible action
has **already run** by the time the plugin logs the violation and drops the
grant cache. **The real gate is the server-side Alter policy** — set
`requires_approval` on every write-capable grant in the Alter dashboard, and
alert on the `HITL VIOLATION` ERROR log line.
- **Process-local state.** Rate-limit buckets, the in-flight `listGrants`
coalescer, and the per-session approval lock are per-process. A multi-replica
gateway needs a shared store; today the gate assumes a single gateway process.
- **No silent IdP refresh.** Expired JWTs re-prompt sign-in rather than refresh.
- **Backend error text is surfaced to the agent.** Non-retryable provider/Alter
4xx messages are relayed to the LLM so it can tell the user accurately — this
assumes the upstream 4xx messages are themselves user-safe.
Found a vulnerability? See **[SECURITY.md](./SECURITY.md)** — please don't open
a public issue.
## Build / test
```sh
pnpm install
pnpm build # tsc → dist/
pnpm typecheck
pnpm test # vitest, 30 tests covering the security primitives
```
## Limitations (v0.0.1)
These are **explicit non-goals** for now:
- **No auto-resume after HITL.** User has to type `/alter status clear` after
approving in the wallet (or just ask again — the lock check is the gate).
Webhook-driven auto-resume is v0.1 work.
- **No silent IdP refresh.** When a JWT expires, user is re-prompted to sign
in. Refresh-token flow is v0.1 work.
- **No per-user LLM billing.** OpenClaw's model-provider plugin holds one
LLM key; the operator pays for all LLM calls. Per-user LLM keys are v0.2.
- **No MCP server mode.** The whole identity gate is built around OpenClaw's
session/channel model; MCP transports need a different identity binding.
Deferred.
- **Per-day rate-limit only** (no per-minute). The counter increments
post-call so per-day caps tolerate the burst at the cap edge.
## Files
- `src/` — TypeScript source (~22 files).
- `tests/` — vitest unit tests for the security primitives.
- `openclaw.plugin.json` — manifest (id, configSchema with
`additionalProperties: false`).
- `OPERATOR_SETUP.md` — provisioning recipe (read this before installing).
## License
MIT.
tools
Comments
Sign in to leave a comment