@human.tech/plugin-waap
v0.1.2
Published
WaaP wallet plugin for ElizaOS — 2PC-MPC signing for EVM and Sui
Readme
@humantech/plugin-waap
ElizaOS plugin for the WaaP wallet — 2-of-2 MPC signing for EVM and Sui transactions, with native 2FA support, server-enforced spending policies, and zero private-key exposure to the agent process.
Status: Full EVM + Sui support. 14 actions, 204 tests.
Why this plugin (vs. Coinbase AgentKit)
| Property | Coinbase AgentKit | @humantech/plugin-waap |
| --------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
| Wallet model | Operator-held API keys | 2-of-2 MPC, no single party holds the key |
| Credential surface in agent process | API keys in env vars | None — only a session file path |
| Approval flow for high-risk txs | None | Native 2FA via Telegram/email/external wallet, surfaced to the user via Eliza callback |
| Spend limit enforcement | Application-level | Server-side policy engine |
| Plugin can sign without user permission | Yes (holds keys) | No (2FA required unless explicitly disabled or permission token issued) |
Installation
# Option A: Add to your character.json plugins array (auto-installed on agent start)
# "plugins": ["@humantech/plugin-waap"]
# Option B: Install explicitly
pnpm add @humantech/plugin-waapMonorepo note: Inside this monorepo the plugin depends on
@human.tech/waap-cliviaworkspace:^, so it always picks up the local CLI build (including in-progress prereq work for0.2.0). At publish time,pnpm publishautomatically rewritesworkspace:^→ a caret range against the actual published version (e.g.^0.2.0), so the artifact on npm depends on a real semver range that allows compatible upgrades.Dev prerequisite: Because
workspace:^symlinks to the CLI's source directory (not itsdist/), you must build the CLI once before the plugin'sWaapService.start()can resolve the binary at runtime:pnpm --filter @human.tech/waap-cli buildRe-run after pulling new CLI changes. (Plugin unit tests don't need this — they spawn a
fake-cli.tsfixture viatsx. Only the production runtime path needs the real built binary.)
Getting Started
No provisioning needed. The plugin starts in unauthenticated mode and lets users create or connect wallets through chat:
- Add
@humantech/plugin-waapto your agent's plugins (via character.json or the web UI) - Start your agent
- Chat: "Create a wallet" — the agent will ask for email/password and create a WaaP account
- Or chat: "Log in with my email" — to connect an existing account
All settings are optional and auto-configured:
- Session storage:
~/.eliza/<agentId>/waap/(auto-generated per agent) - Chain: Ethereum mainnet (switchable via "switch to polygon")
- RPC: Auto-resolved from chainid.network
Environment variables
| Variable | Required | Default | Description |
| --------------------------------- | -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| WAAP_CLI_SESSION_DIR | no | ~/.eliza/<agentId>/waap | Per-agent session directory. Operator runs waap-cli login here. |
| WAAP_DEFAULT_CHAIN | no | 1 (Ethereum mainnet) | Default chain. Accepts evm:137, sui:mainnet, polygon, 1, etc. Switchable via WAAP_SWITCH_CHAIN. |
| WAAP_DEFAULT_CHAIN_ID | no | (deprecated) | Fallback for WAAP_DEFAULT_CHAIN. Use WAAP_DEFAULT_CHAIN instead. |
| WAAP_DEFAULT_RPC_URL | no | (CLI's default) | RPC URL for the configured chain. |
| WAAP_PERMISSION_TOKEN_<chainId> | no | — | Pre-issued permission token to bypass 2FA for a specific chain. Bearer credential — treat as a secret. Rotate. |
| WAAP_CLI_BINARY | no | resolved from node_modules | Override path to the waap-cli binary (escape hatch). |
| SILK_NODE_ENV | no | production | WaaP backend target — development or production. |
Security model
- Password handling in signup/login only. During
WAAP_SIGNUPandWAAP_LOGIN, the password is passed directly to the CLI subprocess and never stored in ElizaOS memory, database, or response callbacks. After authentication, only the session token is retained. - No private key exposure. WaaP uses 2-of-2 MPC; the encrypted keyshare lives in the session directory and is never read by the plugin. This is the headline differentiator vs. plugins that hold raw keys.
- Per-agent isolation.
WAAP_CLI_SESSION_DIRis derived fromruntime.agentIdso two agents on one host get two independent session files. - No
shell: true. All CLI invocations spawn viaargvarrays — zero command-injection surface. - Trust-gated financial actions. Per
@elizaos/plugin-trustcomponentDefaults, all signing/sending/policy actions are disabled by default. Operators must explicitly enable them per character.
Actions (14 total)
| Action | Default | Permissions | Chains | What it does |
| ---------------------- | -------- | ----------- | -------- | --------------------------------------------------------- |
| WAAP_SIGNUP | enabled | (none) | N/A | Create a new WaaP wallet account with email/password |
| WAAP_LOGIN | enabled | (none) | N/A | Log in to an existing WaaP account |
| WAAP_LOGOUT | enabled | (none) | N/A | Log out and clear session |
| WAAP_SWITCH_CHAIN | enabled | (none) | EVM, Sui | Switch active chain (e.g. "switch to polygon", "use sui") |
| WAAP_GET_BALANCE | enabled | (none) | EVM, Sui | Read native balance (ETH or SUI) — no 2FA |
| WAAP_2FA_STATUS | enabled | (none) | N/A | Check current 2FA method |
| WAAP_REQUEST | enabled | (none) | EVM only | Generic EIP-1193 JSON-RPC request — rejects on Sui |
| WAAP_SIGN_MESSAGE | disabled | financial | EVM, Sui | EIP-191 personal_sign (EVM) / native sign (Sui) |
| WAAP_SIGN_TYPED_DATA | disabled | financial | EVM only | EIP-712 structured data sign — rejects on Sui |
| WAAP_SIGN_TX | disabled | financial | EVM, Sui | Sign transaction without broadcasting |
| WAAP_SEND_TX | disabled | financial | EVM, Sui | Sign + broadcast transaction (ETH for EVM, MIST for Sui) |
| WAAP_SET_POLICY | disabled | admin | N/A | Update wallet's daily spend limit |
| WAAP_ENABLE_2FA | disabled | admin | N/A | Enable 2FA (email, telegram, or external wallet) |
| WAAP_DISABLE_2FA | disabled | admin | N/A | Disable 2FA (requires current-method approval) |
To enable financial actions for a specific character, override componentDefaults in the character config — see character.json for an example.
Provider
waapWallet injects both wallet addresses, the active chain, 2FA method, and daily spend limit into the agent's prompt context every turn. ~80 tokens. Cached — does not call the CLI per turn.
The provider outputs text like:
Wallet: authenticated
EVM address: 0xabc123...
Sui address: 0x7f8e9d... (64 hex chars)
Active chain: sui:mainnetAnd exposes structured values:
{
waapAddress: string, // active address (EVM or Sui depending on chain)
waapEvmAddress: string, // always available
waapSuiAddress: string, // always available
waapChainCanonical: 'evm:137' | 'sui:mainnet',
waapChainFamily: 'evm' | 'sui',
waapEvmChainId: number | undefined,
waapSuiNetwork: string | undefined,
waap2faMethod: 'email' | 'telegram' | 'external_wallet' | 'phone' | 'disabled',
waapDailyLimitUsd: number | undefined
}2FA flow
When a wallet has 2FA enabled and the agent attempts a signing operation, the plugin:
- Submits the request to the policy engine
- Receives
awaiting_2faevent from the CLI - Sends a callback message to the user: "Approve this transaction in Telegram. I'll wait up to 5 minutes."
- Waits up to 5 minutes for approval (the CLI handles the WebSocket + HTTP poll fallback internally)
- On approval, completes the 2PC signature and reports the result
Phone 2FA is not supported. The plugin refuses to start if phone_authz is the configured method (the CLI's stdin OTP prompt can't be proxied through a non-TTY subprocess). Switch to telegram, email, or external_wallet via waap-cli 2fa enable --telegram <chatId>.
Troubleshooting
| Error code | Cause | Fix |
| ---------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| NO_SESSION | No logged-in session exists | Chat "create a wallet" or "log in" to authenticate. Or provision manually via waap-cli login. |
| PHONE_2FA_UNSUPPORTED | Wallet has phone 2FA enabled | Switch to telegram/email/external_wallet via waap-cli 2fa enable |
| POLICY_REJECTED | Daily spend limit exceeded or other policy rule | Use WAAP_SET_POLICY to raise the limit (with operator approval) |
| INSUFFICIENT_FUNDS | Wallet doesn't have enough balance + gas | Top up the wallet |
| TWO_FA_TIMEOUT | User didn't approve within 5 minutes | Try again |
| NETWORK | Backend or RPC unreachable | Check connectivity, retry |
| CLI_NOT_FOUND / CLI_VERSION_MISMATCH | waap-cli binary missing or too old | pnpm install to refresh; or set WAAP_CLI_BINARY env var |
The PHONE_2FA_UNSUPPORTED error throws at agent boot — the agent will refuse to start until it's fixed. NO_SESSION no longer throws at boot; the plugin starts in unauthenticated mode and prompts the user to sign up or log in. Other errors are reported via the action's callback to the user.
Developing and testing locally
This section walks through running the plugin end-to-end on your machine. There are four testing layers — start with (1) and only move to (4) when you have real WaaP credentials.
Do I need to install ElizaOS?
Short answer: only for Layer 4. Layers 1–3 validate the plugin without booting an ElizaOS agent at all.
| Layer | Needs ElizaOS installed? | Why |
| ------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. Unit tests | ❌ No | vitest runs the tests against mocked IAgentRuntime objects. @elizaos/core is installed as a dev/runtime dep of the plugin (so the package's tsconfig can resolve its types), but nothing boots the real ElizaOS runtime. Pure pnpm install && pnpm test. |
| 2. Type-check + build | ❌ No | tsc --noEmit and tsup only need the TypeScript definitions from @elizaos/core, which come in as a normal dep. No runtime, no agent. |
| 3. Runtime smoke | ❌ No | Uses the WaapService.startWithRunner(runtime, runner) test helper to inject a mock runner and a plain object runtime. The built dist/index.cjs is loaded via node -e. No ElizaOS agent ever starts. |
| 4. Full integration | ✅ Yes | This is the only layer that boots a real agent with the plugin loaded. You need either (a) @elizaos/cli to run elizaos start against a character.json, or (b) a standalone Node script that instantiates AgentRuntime from @elizaos/core directly. See Layer 4 setup below. |
Why you don't need ElizaOS for dev: the plugin's architecture deliberately isolates runtime dependencies behind interfaces. WaapService extends Eliza's Service class but can be instantiated and exercised with a minimal fake runtime that implements only agentId, getSetting, getService, and (for extractor-using actions) useModel + composeState. The test files in test/unit/ demonstrate the fake-runtime pattern — read them as a reference if you want to write your own integration harness without installing @elizaos/cli.
Prerequisites
- Node.js ≥ 20 (run
node --versionto check) - pnpm ≥ 8.15.5 (the monorepo is pnpm-based; see root
package.jsonpackageManagerfield for the pinned version) - This monorepo checked out with the feature branch (
feature/eliza-plugin-waapor wherever the plugin work lives) - macOS/Linux — the CLI has been tested primarily on these. Windows via WSL should work but isn't validated.
- For Layer 4 only: either
@elizaos/cliglobally installed (npm i -g @elizaos/cli) OR a standalone Node harness script
1. Install dependencies (one-time per clone)
From the monorepo root (not the plugin directory):
cd /path/to/monorepo
pnpm installThis installs deps for every workspace package and creates the symlink packages/eliza-plugin-waap/node_modules/@human.tech/waap-cli → packages/waap-cli (because the plugin depends on workspace:^).
2. Build @human.tech/waap-cli first (required for runtime — optional for unit tests)
Because the plugin's @human.tech/waap-cli dep is a workspace symlink pointing at source, you must build the CLI once so that dist/index.js (the bin entry) actually exists on disk:
pnpm --filter @human.tech/waap-cli buildRe-run after pulling new CLI changes. If you skip this step, WaapService.start() will throw CLI_NOT_FOUND at runtime (plugin unit tests don't need it — they use a fake-cli.ts fixture instead).
3. Layer 1 — Unit tests (fastest, no backend needed)
Runs the full test suite (204 tests across 19 files) against mocks. No network, no real WaaP backend, no credentials. Completes in ~3 seconds.
# Plugin package tests (204 tests)
pnpm --filter @humantech/plugin-waap test
# Or run everything at once from the monorepo root
pnpm -r testWhat these cover (204 tests across 19 files):
errors.test.ts,types.test.ts— error taxonomy + discriminated types (ChainFamily, WaapChainState, ChainId)chains.test.ts(19 cases) — resolveChain() for EVM names, numeric IDs, Sui networkscliRunner.test.ts(16 cases) — subprocess spawning, NDJSON parsing, timeout, AbortSignal, stderr toleranceWaapService.test.ts(19 cases) — init sequence, dual-address parsing, Sui chain switching,--chainflag, signTypedData/request Sui rejection, policy cachingserviceLifecycle.test.ts(23 cases) — unauthenticated mode, login→balance flow, Sui lifecycle, logoutactions/*.test.ts(9 files, ~80 cases) — all 14 actions: validate(), happy path, 2FA flow, Sui chains, param extractionprovider.test.ts— dual-address rendering, active chain displayeventRendering.test.ts— 2FA prompt rendering includingexternal_walletconfirmUrlparamExtraction*.test.ts— zod schemas, LLM extractor (EVM + Sui templates), regex extractor for EIP-712
Expect: 204/204 passing.
4. Layer 2 — Type-check + build (catches regressions tests can miss)
pnpm --filter @humantech/plugin-waap type-check # tsc --noEmit
pnpm --filter @humantech/plugin-waap build # tsup → dist/{index.js, index.cjs, index.d.ts, index.d.mts}If type-check fails, the plugin has a real type-safety issue that must be fixed before shipping. If build fails, the dist won't be publishable.
5. Layer 3 — Runtime smoke test (no WaaP backend, no credentials)
This exercises the full plugin boot + action handler flow using a mock runtime and a mock CliRunner. No real subprocess, no real WaaP backend — but it verifies:
- The built
dist/index.cjsloads correctly from outside the plugin directory resolveBinary()finds the workspace-linked CLI- Service initialization sequence runs (whoami → policy → 2fa)
- Action
handler()returns properActionResultobjects - Eliza's
Contentcallback shape is correct
# Run from any directory
cd /tmp
WAAP_CLI_BINARY= node -e "
const m = require('/absolute/path/to/packages/eliza-plugin-waap/dist/index.cjs');
// 1. Manifest shape
const p = m.waapPlugin;
console.log('plugin:', p.name);
console.log('actions:', p.actions.map(a => a.name));
console.log('service type:', m.WaapService.serviceType);
// 2. Resolve binary
m.createCliRunner().resolveBinary()
.then(b => console.log('binary:', b))
.catch(e => console.log('FAIL:', e.code, e.message));
// 3. Instantiate service via test helper + mock runner
const mockRunner = {
async resolveBinary() { return '/fake'; },
async run(opts) {
if (opts.cmd === 'whoami') return { ok: true, result: { address: '0xabc' } };
if (opts.cmd === 'policy') return { ok: true, result: { policy: { authorization_method: { Disabled: null } } } };
if (opts.cmd === '2fa') return { ok: true, result: { method: 'disabled' } };
if (opts.cmd === 'sign-message') return { ok: true, result: { signature: '0xdead' } };
throw new Error('unexpected: ' + opts.cmd);
}
};
const mockRuntime = {
agentId: 'smoke-test',
getSetting: k => k === 'WAAP_DEFAULT_CHAIN_ID' ? '137' : undefined,
getService: () => null
};
m.WaapService.startWithRunner(mockRuntime, mockRunner).then(async svc => {
console.log('isReady:', svc.isReady());
console.log('address:', svc.getAddress());
console.log('chain:', svc.getCanonicalChain());
const sig = await svc.signMessage({ message: 'hi' });
console.log('sig:', sig.signature);
console.log('=== SMOKE PASSED ===');
});
"Expected output (adjust /absolute/path/to/):
plugin: @humantech/plugin-waap
actions: [ 'WAAP_SEND_TX', 'WAAP_SIGN_MESSAGE', 'WAAP_SIGN_TYPED_DATA', 'WAAP_GET_BALANCE', 'WAAP_SET_POLICY', 'WAAP_SIGNUP', 'WAAP_LOGIN', 'WAAP_SWITCH_CHAIN' ]
service type: wallet
binary: /.../packages/waap-cli/dist/index.js
isReady: true
address: 0xabc
chain: evm:137
sig: 0xdead
=== SMOKE PASSED ===If any of these print FAIL: or don't print, something is wrong with the built artifact. Rebuild and re-run.
6. Layer 4 — Full integration against a real WaaP backend (requires credentials)
This is the only layer that actually talks to the WaaP backend, signs with a real MPC keyshare, and broadcasts transactions. Requires a real test wallet with funds on whichever chain you're exercising.
Setup:
# 1. Pick a temp session directory (per-agent isolation — CLI creates it automatically on login)
export WAAP_CLI_SESSION_DIR=/tmp/waap-integration-$(date +%s)
# 2. Log the CLI in (one-time — this is the only place your password is entered)
./packages/waap-cli/dist/index.js login \
-e [email protected] \
-p 'your-password'
# 3. Verify the session works
./packages/waap-cli/dist/index.js whoami
# Expected: Wallet address: 0x...
# 4. Verify JSON mode works (this is what the plugin uses)
./packages/waap-cli/dist/index.js --json whoami
# Expected: one line of NDJSON:
# {"event":"result","ok":true,"address":"0x..."}
# 5. Verify NO_SESSION error path (point at an empty dir)
WAAP_CLI_SESSION_DIR=/tmp/waap-empty ./packages/waap-cli/dist/index.js --json whoami
# Expected:
# {"event":"error","message":"No session found...","code":"NO_SESSION"}Exercise the plugin end-to-end:
Create a minimal Eliza character config (test-character.json) that loads the plugin:
{
"name": "waap-smoke",
"plugins": ["@humantech/plugin-waap"],
"settings": {
"WAAP_CLI_SESSION_DIR": "/tmp/waap-integration-...",
"WAAP_DEFAULT_CHAIN_ID": "1"
},
"bio": ["Test agent for WaaP plugin smoke testing"]
}Then boot an Eliza agent with this character and interact via your normal Eliza frontend. Try:
- "What's my wallet address?" → should hit
waapWalletProvidercache, return address in context (no action call) - "What's my balance?" → invokes
WAAP_GET_BALANCE, returns ETH amount - "Sign the message 'hello'" → invokes
WAAP_SIGN_MESSAGE, returns signature - "Send 0.001 ETH to 0x..." → invokes
WAAP_SEND_TX; if 2FA is enabled, you should see the approval callback prompting you to approve in Telegram/email/hardware wallet, then the tx hash
What to watch for:
- The provider context should include
waapAddress,waapChainCanonical,waap2faMethod,waapDailyLimitUsdin every agent turn WAAP_GET_BALANCEshould never trigger 2FA (it's read-only, enabled by default)WAAP_SEND_TX/WAAP_SIGN_MESSAGE/WAAP_SIGN_TYPED_DATA/WAAP_SET_POLICYare disabled by default — you must either enable them incharacter.actionPermissionsor load@elizaos/plugin-trustand configure the trust layer- 2FA flow timing: the plugin waits up to 5 minutes for approval (inherited from the CLI's internal WebSocket + poll fallback)
7. Running the existing CLI integration test suite (optional, bash)
The CLI ships its own bash test runner at packages/waap-cli/test-cli.sh that exercises every CLI command (including the new --json mode). If you have a logged-in session:
cd packages/waap-cli
# Run the human-mode test suite (default)
./test-cli.sh
# Run in opt-in JSON mode (exercises the plugin's contract surface)
WAAP_CLI_JSON_MODE=1 ./test-cli.shThe script verifies every signature with viem.recoverMessageAddress / viem.recoverTypedDataAddress / viem.recoverTransactionAddress to catch any regression in the 2PC signing pipeline.
8. Debugging tips
- Tests hang or flake on
cliRunnertimeout cases: thefake-cli.tsfixture usestsxat runtime. First run can be slow; the test suite has generous timeouts buthardTimeoutMs: 500tests may flake on cold caches. Re-run. CLI_NOT_FOUNDat runtime: you skipped step 2 (build the CLI). Runpnpm --filter @human.tech/waap-cli buildand retry.NO_SESSIONeven though you ranlogin: the session dir the plugin computes (~/.eliza/<agentId>/waapby default) probably differs from where you logged in. Either setWAAP_CLI_SESSION_DIRexplicitly on both the login and the agent boot, or log in to the auto-derived path.- Phone 2FA errors with
PHONE_2FA_UNSUPPORTED: the plugin refuses to boot with phone 2FA because stdin OTP can't be proxied through a non-TTY subprocess. Switch the account's 2FA method:waap-cli 2fa enable --telegram <chatId>(or email / external_wallet). - Need verbose CLI output in JSON mode: you can't —
--jsonsuppresses decorated stdout on purpose. Tail stderr instead (2>&1 | grep -v '^{'). - Type errors about
@types/chai/@types/nodeBuffer conflicts: these are a known root-level tsc context issue in this monorepo, unrelated to the plugin. Usepnpm --filter @humantech/plugin-waap type-check(the package-local tsc) which hasskipLibCheck: trueand passes cleanly.
Why vitest instead of bun:test?
The official ElizaOS plugin docs recommend tsup for build + bun test for tests. We use tsup for build + vitest for tests — a deliberate, narrow divergence on the test runner only. The build tool matches the docs exactly.
Why:
- This plugin lives in a pnpm-based monorepo. Every other package under
packages/here uses vitest or jest. Adding bun would make this the one outlier and force every developer who works across packages to install and learn a second test runner. - The 132 existing tests use vitest-specific patterns that don't translate 1:1 to
bun:test'smock.module()API — particularly the action tests'vi.mock(..., async () => { const actual = await vi.importActual(...); return { ...actual, extractFn: vi.fn() } })pattern and the storage tests' dynamic-import cache-busting trick. - Test runner choice has zero impact on the published artifact. End users on Node, on bun-powered ElizaOS Cloud, or anywhere else see an identical
dist/index.cjs. Test infrastructure never crosses the package boundary. - The Eliza docs themselves are inconsistent — they recommend
tsup(a Node-ecosystem bundler) for the build step, which signals that mixing tool ecosystems within a plugin is expected and acceptable.
Note for contributors: if you're copying test patterns from upstream Eliza plugin examples (which use bun:test), you'll need to translate the mock API. The structural shape (describe/it/expect/beforeEach) is identical; only the mock helpers differ (mock() → vi.fn(), mock.module() → vi.mock(), spyOn() → vi.spyOn()).
Quick "everything still works" check
# From monorepo root
pnpm install && \
pnpm --filter @human.tech/waap-cli build && \
pnpm --filter @human.tech/waap-cli test && \
pnpm --filter @humantech/plugin-waap type-check && \
pnpm --filter @humantech/plugin-waap test && \
pnpm --filter @humantech/plugin-waap build && \
echo "✅ all green"If that prints ✅ all green, the plugin is healthy at Layers 1-3. Only Layer 4 requires real credentials.
Testing in production (after publish to npm)
Once @humantech/plugin-waap is published to npm, you need a separate testing path to verify the published artifact works — not the local workspace checkout. Production testing catches a different class of bugs: missing files in the published tarball, broken dependency resolution in a fresh install, version drift between what you built locally and what npm actually got.
1. Pre-publish: dry-run the published tarball
Before pnpm publish (or via changesets), inspect exactly what will ship to npm:
cd packages/eliza-plugin-waap
pnpm pack
# → Creates humantech-plugin-waap-0.1.0.tgz in the current directory
# Inspect contents
tar -tzf humantech-plugin-waap-0.1.0.tgz | sortExpected contents (matches the "files" field in package.json):
package/LICENSE
package/README.md
package/dist/index.cjs # CJS bundle
package/dist/index.cjs.map
package/dist/index.d.mts # ESM type declarations
package/dist/index.d.ts # CJS type declarations
package/dist/index.js # ESM bundle
package/dist/index.js.map
package/package.jsonTarball size should be ~70 KB. If it's substantially larger (e.g. 500 KB+) and you see extra chunks like chunk-*.js, _esm-*.js, secp256k1-*.js, that means viem (or another runtime dep) is being inlined. Check tsup.config.ts — every runtime dep must be in the external: [...] array so tsup doesn't bundle it.
Things to verify in the unpacked package.json:
mkdir -p /tmp/waap-unpack && tar -xzf humantech-plugin-waap-0.1.0.tgz -C /tmp/waap-unpack
cat /tmp/waap-unpack/package/package.json | grep -A 6 '"dependencies"'"@human.tech/waap-cli"must be a real semver range (e.g."^0.2.0"), not"workspace:^".pnpm publishrewrites it automatically at publish time, but verify — if you seeworkspace:in the published tarball, the publish pipeline is broken and npm installers will fail."@elizaos/core","@elizaos/plugin-trust","zod"all have concrete versions.- No dev deps bleeding into
dependencies.
2. Fresh-install smoke test in a scratch directory
Simulate what a real end user sees when they install the plugin cold:
# Scratch directory outside the monorepo
mkdir -p /tmp/waap-published-smoke && cd /tmp/waap-published-smoke
# Initialize a minimal package
npm init -y
npm pkg set type=module
# Install the published plugin (or pack file for pre-release testing)
# Option A: real published version
npm install @humantech/plugin-waap
# Option B: locally-packed tarball (pre-release)
npm install /path/to/humantech-plugin-waap-0.1.0.tgzThen verify the install produced a working artifact:
# 1. Can you require it at all?
node -e "const m = require('@humantech/plugin-waap'); console.log('name:', m.waapPlugin.name); console.log('actions:', m.waapPlugin.actions.map(a => a.name))"Expected:
name: @humantech/plugin-waap
actions: [ 'WAAP_SEND_TX', 'WAAP_SIGN_MESSAGE', 'WAAP_SIGN_TYPED_DATA', 'WAAP_GET_BALANCE', 'WAAP_SET_POLICY', 'WAAP_SIGNUP', 'WAAP_LOGIN', 'WAAP_SWITCH_CHAIN' ]# 2. Can the bundled cliRunner resolve @human.tech/waap-cli from the fresh node_modules?
node -e "const m = require('@humantech/plugin-waap'); m.createCliRunner().resolveBinary().then(p => console.log('binary:', p)).catch(e => console.log('FAIL:', e.code, e.message))"Expected:
binary: /tmp/waap-published-smoke/node_modules/@human.tech/waap-cli/dist/index.jsIf this prints FAIL: CLI_NOT_FOUND, the published @human.tech/waap-cli tarball is missing its dist/ directory (its "files" field or build-to-publish script is broken).
# 3. Can the CLI actually run in JSON mode?
./node_modules/.bin/waap-cli --json --help 2>&1 | head -5Expected: the commander help output (not necessarily JSON-formatted since --help is a commander built-in, but the command should exit 0).
3. Canary smoke test against a real WaaP backend
Same setup as Layer 4 above, but against the published plugin version and against the production WaaP backend (or a dedicated staging backend if you have one). This is the only test that confirms real MPC signing + real policy engine + real 2FA flow work end-to-end with the published artifact.
cd /tmp/waap-published-smoke
# Login against production (use a dedicated canary test wallet, not a real user's wallet)
export WAAP_CLI_SESSION_DIR=/tmp/waap-canary-$(date +%s)
./node_modules/.bin/waap-cli login \
-e [email protected] \
-p 'your-canary-password'
# Verify session + JSON mode
./node_modules/.bin/waap-cli --json whoamiThen boot a minimal ElizaOS agent (see Layer 4) pointing at this published install. Run the same prompt battery:
- "What's my wallet address?" — provider context only
- "What's my balance?" —
WAAP_GET_BALANCE - "Sign the message 'canary test'" —
WAAP_SIGN_MESSAGE(with 2FA flow if enabled) - "Send 0.0001 ETH to " —
WAAP_SEND_TX(low-value canary tx)
Record the tx hash from the last step. Verify on a block explorer that it actually landed on-chain and the from address matches the canary wallet.
4. Monitoring after deploy (whoever hosts the agents)
Once the plugin is deployed in production agents, monitor for:
NO_SESSIONerrors at agent startup → operator didn't provision correctly, or the session dir got wipedTWO_FA_TIMEOUTerrors → users aren't approving within 5 minutes (UX problem — possibly need to surface the approval prompt more prominently)POLICY_REJECTEDerrors → spend-limit tuning needed, or legitimate policy enforcement working as designedCLI_PROTOCOLerrors → the CLI is emitting something the plugin can't parse (e.g. after a CLI upgrade introduces a new event shape). This is the main signal that a CLI/plugin version mismatch happened.CLI_NOT_FOUND/CLI_VERSION_MISMATCH→ install pipeline problem- Telegram/email approval flows not surfacing to users → check that
renderEventoutput is actually making it through Eliza'scallback()into the user-visible chat; the plugin can't tell by itself if the user saw the prompt
5. Rollback strategy
If a canary or production smoke fails catastrophically:
- For the plugin: publish a patch release reverting to the last known-good behavior. npm has no "unpublish" for versions older than 72 hours, so always roll forward rather than try to delete.
- For the CLI:
waap-cliis pinned by semver range in the plugin'spackage.json. If a bad CLI version ships, either (a) publish a plugin patch that tightens the range ("^0.2.0"→"0.2.3"), or (b) publish a fixed CLI patch release. The second is usually faster because the plugin doesn't need to re-release. - For the backend: unchanged by this plugin — WaaP backend incidents are handled in the backend team's ops runbook.
6. What NOT to test in production
- Do not test with your real personal wallet. Always use a dedicated canary account with minimal funds.
- Do not test
WAAP_SET_POLICYagainst a real wallet in production — it permanently changes the wallet's spend limit (reversible, but via another signed policy mutation). - Do not disable 2FA on a real wallet to speed up testing. Canary account only.
- Do not run automated production tests that call real sign/send actions on a loop — even small value txs compound into gas spam.
Quick production smoke checklist
[ ] `pnpm pack` inspection — tarball contains expected files
[ ] package.json dependencies rewritten (no workspace: references)
[ ] Fresh npm install in scratch dir succeeds
[ ] `require('@humantech/plugin-waap').waapPlugin` loads cleanly
[ ] `createCliRunner().resolveBinary()` finds the CLI binary
[ ] Canary wallet logs in via published CLI
[ ] `--json whoami` emits expected NDJSON
[ ] Eliza agent boots with plugin loaded (no errors at startup)
[ ] Provider context includes wallet address
[ ] `WAAP_GET_BALANCE` returns a balance
[ ] `WAAP_SIGN_MESSAGE` returns a valid signature (verify with viem)
[ ] `WAAP_SEND_TX` produces a real tx hash confirmed on-chain
[ ] 2FA flow surfaces approval prompt to user via callback
[ ] No `CLI_PROTOCOL` / `CLI_VERSION_MISMATCH` errors in logsArchitecture notes
- Consumes
@human.tech/waap-cliviaworkspace:^in this monorepo (on npm publish, rewritten to a caret range). - LLM-driven param extractors use the canonical Eliza 1.x pattern (
composePromptFromState+runtime.useModel(ModelType.TEXT_SMALL)+parseJSONObjectFromText+ zod validation), with regex-based fallback forWAAP_SIGN_TYPED_DATA(EIP-712 JSON blobs are too complex for reliable LLM round-trip). - Dual-address model: a single keyshare derives both an EVM and Sui address. Both stored in state, shown in provider context.
getAddress()returns the one matching the active chain. - In-memory chain state with explicit
--chainflag on every CLI call — avoids side effects on the CLI session file.
Design docs
The full design specs and implementation plans live in docs/superpowers/specs/ and docs/superpowers/plans/:
001-eliza-plugin-waap-design-2026-04-07.md— original EVM-only design002-eliza-plugin-waap-production-2026-04-10.md— production readiness spec (audit fixes, new actions)003-eliza-plugin-waap-sui-2026-04-13.md— Sui integration spec004-sui-branch-audit-2026-04-14.md— 3-branch comparison audit005-cli-vs-plugin-actions-2026-04-14.md— CLI vs plugin action mapping
