@wolpertingerlabs/drawlatch 1.0.0-alpha.4 → 1.0.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONNECTIONS.md +9 -1
- package/README.md +393 -465
- package/bin/drawlatch.js +1149 -47
- package/dist/auth/auth.d.ts +10 -0
- package/dist/auth/auth.js +146 -0
- package/dist/auth/env-writer.d.ts +6 -0
- package/dist/auth/env-writer.js +52 -0
- package/dist/auth/password.d.ts +8 -0
- package/dist/auth/password.js +28 -0
- package/dist/auth/sessions.d.ts +13 -0
- package/dist/auth/sessions.js +85 -0
- package/dist/cli/generate-keys.d.ts +4 -4
- package/dist/cli/generate-keys.js +30 -25
- package/dist/connections/{anthropic.json → ai/anthropic.json} +2 -0
- package/dist/connections/{devin.json → ai/devin.json} +2 -0
- package/dist/connections/ai/exa.json +24 -0
- package/dist/connections/{google-ai.json → ai/google-ai.json} +2 -0
- package/dist/connections/{openai.json → ai/openai.json} +2 -0
- package/dist/connections/{openrouter.json → ai/openrouter.json} +2 -0
- package/dist/connections/developer-tools/datadog.json +27 -0
- package/dist/connections/{github.json → developer-tools/github.json} +59 -3
- package/dist/connections/{hex.json → developer-tools/hex.json} +2 -0
- package/dist/connections/{linear.json → developer-tools/linear.json} +2 -0
- package/dist/connections/{lichess.json → gaming/lichess.json} +2 -0
- package/dist/connections/messaging/agentmail.json +21 -0
- package/dist/connections/{discord-bot.json → messaging/discord-bot.json} +2 -0
- package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +2 -0
- package/dist/connections/{slack.json → messaging/slack.json} +2 -0
- package/dist/connections/{telegram.json → messaging/telegram.json} +2 -0
- package/dist/connections/{google.json → productivity/google.json} +2 -0
- package/dist/connections/{notion.json → productivity/notion.json} +2 -0
- package/dist/connections/{stripe.json → productivity/stripe.json} +2 -0
- package/dist/connections/{trello.json → productivity/trello.json} +2 -0
- package/dist/connections/{bluesky.json → social-media/bluesky.json} +2 -0
- package/dist/connections/{mastodon.json → social-media/mastodon.json} +2 -0
- package/dist/connections/{reddit.json → social-media/reddit.json} +2 -0
- package/dist/connections/{twitch.json → social-media/twitch.json} +2 -0
- package/dist/connections/{x.json → social-media/x.json} +2 -0
- package/dist/mcp/server.js +268 -33
- package/dist/remote/admin-mutations.d.ts +43 -0
- package/dist/remote/admin-mutations.js +321 -0
- package/dist/remote/admin-types.d.ts +153 -0
- package/dist/remote/admin-types.js +11 -0
- package/dist/remote/admin.d.ts +37 -0
- package/dist/remote/admin.js +317 -0
- package/dist/remote/caller-bootstrap.d.ts +121 -0
- package/dist/remote/caller-bootstrap.js +270 -0
- package/dist/remote/ingestors/base-ingestor.d.ts +5 -0
- package/dist/remote/ingestors/base-ingestor.js +17 -4
- package/dist/remote/ingestors/discord/discord-gateway.js +24 -1
- package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
- package/dist/remote/ingestors/e2e/setup.js +147 -0
- package/dist/remote/ingestors/manager.d.ts +35 -1
- package/dist/remote/ingestors/manager.js +160 -22
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +2 -0
- package/dist/remote/ingestors/poll/poll-ingestor.js +21 -0
- package/dist/remote/ingestors/slack/socket-mode.js +23 -1
- package/dist/remote/ingestors/types.d.ts +8 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +16 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +49 -3
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +9 -9
- package/dist/remote/server.d.ts +25 -33
- package/dist/remote/server.js +595 -735
- package/dist/remote/tool-dispatch.d.ts +84 -0
- package/dist/remote/tool-dispatch.js +910 -0
- package/dist/remote/triggers/rule-engine.d.ts +40 -0
- package/dist/remote/triggers/rule-engine.js +228 -0
- package/dist/remote/triggers/types.d.ts +69 -0
- package/dist/remote/triggers/types.js +10 -0
- package/dist/remote/tunnel-state.d.ts +14 -0
- package/dist/remote/tunnel-state.js +20 -0
- package/dist/remote/tunnel.d.ts +13 -0
- package/dist/remote/tunnel.js +33 -0
- package/dist/shared/config.d.ts +61 -27
- package/dist/shared/config.js +41 -33
- package/dist/shared/connections.d.ts +12 -5
- package/dist/shared/connections.js +52 -14
- package/dist/shared/crypto/index.d.ts +1 -0
- package/dist/shared/crypto/index.js +1 -0
- package/dist/shared/crypto/key-manager.d.ts +81 -0
- package/dist/shared/crypto/key-manager.js +174 -0
- package/dist/shared/env-utils.d.ts +42 -0
- package/dist/shared/env-utils.js +150 -0
- package/dist/shared/migrations.d.ts +40 -0
- package/dist/shared/migrations.js +122 -0
- package/dist/shared/protocol/caller-bundle-crypto.d.ts +37 -0
- package/dist/shared/protocol/caller-bundle-crypto.js +62 -0
- package/dist/shared/protocol/caller-bundle.d.ts +75 -0
- package/dist/shared/protocol/caller-bundle.js +24 -0
- package/dist/shared/protocol/handshake.js +14 -1
- package/dist/shared/protocol/index.d.ts +2 -0
- package/dist/shared/protocol/index.js +2 -0
- package/dist/shared/protocol/sync-client.d.ts +52 -0
- package/dist/shared/protocol/sync-client.js +99 -0
- package/dist/shared/protocol/sync.d.ts +71 -0
- package/dist/shared/protocol/sync.js +176 -0
- package/frontend/dist/assets/index-BdCSPSZK.js +282 -0
- package/frontend/dist/assets/index-BmK26bY2.css +1 -0
- package/frontend/dist/index.html +15 -0
- package/package.json +63 -11
package/README.md
CHANGED
|
@@ -1,686 +1,614 @@
|
|
|
1
1
|
# Drawlatch
|
|
2
2
|
|
|
3
|
-
> **Alpha Software:**
|
|
3
|
+
> **Alpha Software:** Expect breaking changes between updates.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drawlatch is a config-driven proxy that gives AI agents authenticated access to external APIs. Define your connections and secrets in a single config file — agents get structured, allowlisted access to 23 pre-built APIs without ever seeing your credentials.
|
|
6
6
|
|
|
7
|
-
Drawlatch
|
|
7
|
+
**Using [Callboard](https://github.com/WolpertingerLabs/callboard)?** Drawlatch is built in — Callboard manages connections, secrets, and agent identities through its UI. You don't need to set up drawlatch separately.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
- **23 pre-built connections** — GitHub, Slack, Discord, Stripe, Notion, Linear, OpenAI, and [more](CONNECTIONS.md)
|
|
12
|
+
- **Endpoint allowlisting** — agents can only reach explicitly configured URL patterns
|
|
13
|
+
- **Per-caller access control** — each agent identity sees only its assigned connections
|
|
14
|
+
- **Real-time event ingestion** — WebSocket, webhook, and polling listeners for incoming events ([details](INGESTORS.md))
|
|
15
|
+
- **Two operating modes** — remote (secrets on a separate server with E2EE) or local (in-process library)
|
|
11
16
|
|
|
12
17
|
## How It Works
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
Drawlatch runs in two modes depending on your trust model:
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
### Remote Mode — Secrets Never Leave the Server
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
2. **Remote Secure Server** — holds all secrets (API keys, tokens, etc.) and only communicates through encrypted channels after mutual authentication. It injects secrets into outgoing HTTP requests on the proxy's behalf.
|
|
23
|
+
The local MCP proxy holds no secrets. It encrypts requests and forwards them to a remote server that injects credentials and makes the actual API calls.
|
|
20
24
|
|
|
21
25
|
```
|
|
22
|
-
┌──────────────┐
|
|
23
|
-
│ Claude Code
|
|
24
|
-
│ │
|
|
25
|
-
└──────────────┘
|
|
26
|
-
No secrets here Injects API keys,
|
|
27
|
-
tokens, headers
|
|
26
|
+
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
|
|
27
|
+
│ Claude Code │◄── stdio ──► MCP Proxy │◄── HTTP + E2EE ──► Remote Server │── HTTPS ────►│ External API │
|
|
28
|
+
│ │ (no secrets) │ │ (holds secrets) │ │ │
|
|
29
|
+
└──────────────┘ └──────────────────┘ └──────────────┘
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
The crypto layer uses
|
|
32
|
+
The crypto layer uses Ed25519 signatures for mutual authentication and X25519 ECDH to derive AES-256-GCM session keys — all built on Node.js native `crypto` with zero external dependencies.
|
|
31
33
|
|
|
32
|
-
### Local Mode
|
|
34
|
+
### Local Mode — In-Process Library
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
No server, no encryption. Your application imports drawlatch directly and calls the same `executeProxyRequest()` function the remote server uses. Secrets come from `process.env` on the same machine.
|
|
35
37
|
|
|
36
38
|
```
|
|
37
|
-
┌──────────────────────────────────────────┐
|
|
38
|
-
│ Your Application
|
|
39
|
-
│ ┌──────────┐ in-process ┌────────┐ │
|
|
40
|
-
│ │ Agent │◄──
|
|
41
|
-
│ │ │ │ routes │ │ local env / config
|
|
39
|
+
┌──────────────────────────────────────────┐ ┌──────────────┐
|
|
40
|
+
│ Your Application │── HTTPS ──────────►│ External API │
|
|
41
|
+
│ ┌──────────┐ in-process ┌────────┐ │ │ │
|
|
42
|
+
│ │ Agent │◄── call ──────►│ drawl. │ │ └──────────────┘
|
|
42
43
|
│ └──────────┘ └────────┘ │
|
|
43
44
|
└──────────────────────────────────────────┘
|
|
44
|
-
Secrets are on the same machine
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
**What you don't get:** Secret isolation from the agent. When running locally, secrets live in `process.env` on the same machine. The value proposition shifts from cryptographic secret hiding to **convenience and structured access** — a single config file managing many API connections with consistent patterns.
|
|
47
|
+
You still get config-driven route resolution, endpoint allowlisting, per-caller access control, and ingestor support — just without cryptographic secret isolation.
|
|
50
48
|
|
|
51
|
-
> **When to use which
|
|
49
|
+
> **When to use which:** Remote mode when secrets must be hidden from the agent's machine (shared servers, CI, untrusted environments). Local mode when running on your own machine and you want convenience without a separate server.
|
|
52
50
|
|
|
53
51
|
## Quick Start
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
Get from zero to working in three commands:
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
```bash
|
|
56
|
+
# Install globally
|
|
57
|
+
npm install -g @wolpertingerlabs/drawlatch
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
/plugin marketplace add ./path/to/drawlatch
|
|
59
|
+
# Set up keys, config, and .env in one step
|
|
60
|
+
drawlatch init --connections github
|
|
62
61
|
|
|
63
|
-
#
|
|
64
|
-
|
|
62
|
+
# Set your API token (edit the file or run this)
|
|
63
|
+
echo "GITHUB_TOKEN=ghp_your_token_here" >> ~/.drawlatch/.env
|
|
64
|
+
|
|
65
|
+
# Start the remote server
|
|
66
|
+
drawlatch start
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
Verify your setup:
|
|
68
70
|
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
+
```bash
|
|
72
|
+
drawlatch doctor # Validate full setup
|
|
73
|
+
drawlatch status # Check server is running
|
|
74
|
+
drawlatch config # View configuration and secret status
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
The `init` command generates keys, creates configs, exchanges public keys, and scaffolds the `.env` file. All steps are idempotent — safe to re-run.
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
### Connect to Claude Code
|
|
80
|
+
|
|
81
|
+
**Option 1: Claude Code Plugin (Recommended)**
|
|
82
|
+
|
|
83
|
+
```shell
|
|
84
|
+
# Install the plugin
|
|
85
|
+
/plugin install drawlatch@drawlatch
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
The plugin's MCP server starts automatically
|
|
88
|
+
The plugin's MCP server starts automatically. The proxy uses `~/.drawlatch/` by default — see [Advanced Configuration](#advanced-configuration) to use a custom path.
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
**Option 2: Auto-Discovery**
|
|
82
91
|
|
|
83
|
-
This repo includes a `.mcp.json` file
|
|
92
|
+
This repo includes a `.mcp.json` file, so Claude Code automatically discovers the MCP proxy when you open the project. Approve the server when prompted.
|
|
84
93
|
|
|
85
|
-
|
|
94
|
+
**Option 3: Manual Registration**
|
|
86
95
|
|
|
87
96
|
```bash
|
|
88
|
-
|
|
97
|
+
claude mcp add drawlatch \
|
|
98
|
+
-e MCP_CONFIG_DIR=~/.drawlatch \
|
|
99
|
+
-- node /path/to/drawlatch/dist/mcp/server.js
|
|
89
100
|
```
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
> **Note:** Auto-discovery and manual registration use `dist/mcp/server.js`. The `dist/` directory is built automatically via `npm install` (prepare script). Rebuild manually with `npm run build` if needed.
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
### Manual Setup
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
For custom setups (different aliases, multiple callers, different machines), you can configure everything manually instead of using `drawlatch init`.
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
**1. Generate keys:**
|
|
98
109
|
|
|
99
110
|
```bash
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
npm run build
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Directory Structure
|
|
107
|
-
|
|
108
|
-
All config and key files live inside `~/.drawlatch/` in the user's home directory by default. You can override this by setting the `MCP_CONFIG_DIR` environment variable.
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
~/.drawlatch/
|
|
112
|
-
├── proxy.config.json # Local proxy config
|
|
113
|
-
├── remote.config.json # Remote server config
|
|
114
|
-
└── keys/
|
|
115
|
-
├── local/ # MCP proxy keypairs (one per alias)
|
|
116
|
-
│ └── my-laptop/ # Alias-named subdirectory
|
|
117
|
-
│ ├── signing.pub.pem # Ed25519 public key (share this)
|
|
118
|
-
│ ├── signing.key.pem # Ed25519 private key (keep secret)
|
|
119
|
-
│ ├── exchange.pub.pem # X25519 public key (share this)
|
|
120
|
-
│ └── exchange.key.pem # X25519 private key (keep secret)
|
|
121
|
-
├── remote/ # Remote server keypair
|
|
122
|
-
│ ├── signing.pub.pem
|
|
123
|
-
│ ├── signing.key.pem
|
|
124
|
-
│ ├── exchange.pub.pem
|
|
125
|
-
│ └── exchange.key.pem
|
|
126
|
-
└── peers/
|
|
127
|
-
├── alice/ # One subdirectory per caller
|
|
128
|
-
│ ├── signing.pub.pem # Caller's public signing key
|
|
129
|
-
│ └── exchange.pub.pem # Caller's public exchange key
|
|
130
|
-
├── bob/ # Another caller
|
|
131
|
-
│ ├── signing.pub.pem
|
|
132
|
-
│ └── exchange.pub.pem
|
|
133
|
-
└── remote-server/ # Remote server's public keys (for proxy)
|
|
134
|
-
├── signing.pub.pem
|
|
135
|
-
└── exchange.pub.pem
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Step 1: Generate Keys
|
|
139
|
-
|
|
140
|
-
Generate keypairs for both the local proxy and the remote server:
|
|
111
|
+
drawlatch generate-keys caller my-laptop
|
|
112
|
+
drawlatch generate-keys server
|
|
113
|
+
```
|
|
141
114
|
|
|
142
|
-
|
|
143
|
-
# Generate local MCP proxy keypair (with alias)
|
|
144
|
-
npm run generate-keys -- local my-laptop
|
|
115
|
+
**2. Exchange public keys** — on separate machines, copy `*.pub.pem` files to the matching `keys/callers/<alias>/` or `keys/server/` directory on the other machine. See [Key Exchange](#key-exchange) for details.
|
|
145
116
|
|
|
146
|
-
|
|
147
|
-
npm run generate-keys -- local
|
|
117
|
+
**3. Create configs** — copy the example files and edit:
|
|
148
118
|
|
|
149
|
-
|
|
150
|
-
|
|
119
|
+
```bash
|
|
120
|
+
cp remote.config.example.json ~/.drawlatch/remote.config.json
|
|
121
|
+
cp proxy.config.example.json ~/.drawlatch/proxy.config.json
|
|
151
122
|
```
|
|
152
123
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
> **Multiple identities:** Generate multiple local keypairs using different aliases (e.g., `my-laptop`, `ci-server`). Set `MCP_KEY_ALIAS` per agent at spawn time or use `localKeyAlias` in `proxy.config.json` to select which identity the proxy uses. The alias directory name should match the caller alias in the remote server's config.
|
|
156
|
-
|
|
157
|
-
You can also generate keys to a custom directory:
|
|
124
|
+
**4. Create a `.env` file** with your API secrets:
|
|
158
125
|
|
|
159
126
|
```bash
|
|
160
|
-
|
|
127
|
+
cat > ~/.drawlatch/.env << 'EOF'
|
|
128
|
+
# GITHUB_TOKEN=ghp_your_token_here
|
|
129
|
+
# DISCORD_BOT_TOKEN=your_bot_token_here
|
|
130
|
+
EOF
|
|
161
131
|
```
|
|
162
132
|
|
|
163
|
-
|
|
133
|
+
**5. Start the server:**
|
|
164
134
|
|
|
165
135
|
```bash
|
|
166
|
-
|
|
136
|
+
drawlatch start
|
|
137
|
+
drawlatch doctor # Validate full setup
|
|
167
138
|
```
|
|
168
139
|
|
|
169
|
-
|
|
140
|
+
## Admin Dashboard
|
|
170
141
|
|
|
171
|
-
|
|
142
|
+
`drawlatch start` serves a built-in web dashboard — a React single-page app — that **fully manages** your running daemon: enable/disable connections per caller, set and clear secrets, create and delete callers, configure and control event listeners (start/stop/restart, multi-instance management), and watch the live event/log feed — all from the browser, no config-file editing required. Every change is applied with a **live reload** (the daemon re-resolves routes and ingestors in place), so there is no "restart to apply" step. drawlatch owns 100% of its own state through this password-gated surface; nothing external writes its config.
|
|
172
143
|
|
|
173
|
-
|
|
144
|
+
### Architecture
|
|
174
145
|
|
|
175
|
-
|
|
176
|
-
mkdir -p ~/.drawlatch/keys/peers/my-laptop
|
|
146
|
+
There is no separate UI service to run. The React app, the `/api/admin/*` API, and the MCP protocol endpoints (`/handshake`, `/request`, `/events`, `/webhooks`, …) are all served by the **same Express process on the same port** as the daemon (default `http://127.0.0.1:9999/`):
|
|
177
147
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
148
|
+
```
|
|
149
|
+
┌────────────────────────────── drawlatch daemon (one process, port 9999) ──────────────────────────────┐
|
|
150
|
+
│ │
|
|
151
|
+
│ GET / → React SPA (served from frontend/dist in production) │
|
|
152
|
+
│ /api/admin/* → read + mutating JSON API ──┐ │
|
|
153
|
+
│ POST /api/auth/* → login / logout / check ├─ password-gated (session cookie) │
|
|
154
|
+
│ /handshake /request … → MCP protocol (E2EE) ─┘ ← unaffected by dashboard auth │
|
|
155
|
+
│ │
|
|
156
|
+
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
183
157
|
```
|
|
184
158
|
|
|
185
|
-
|
|
159
|
+
Any unmatched non-API GET falls back to `index.html` so client-side routing works. The MCP protocol endpoints are independent of the dashboard — they keep serving agents even when the dashboard is locked (see below).
|
|
186
160
|
|
|
187
|
-
|
|
188
|
-
mkdir -p ~/.drawlatch/keys/peers/remote-server
|
|
161
|
+
### Setup
|
|
189
162
|
|
|
190
|
-
|
|
191
|
-
~/.drawlatch/keys/peers/remote-server/signing.pub.pem
|
|
163
|
+
**1. Set a password** (required — the dashboard is locked until one is set):
|
|
192
164
|
|
|
193
|
-
|
|
194
|
-
|
|
165
|
+
```bash
|
|
166
|
+
drawlatch set-password # prompts on a TTY, or reads a password piped on stdin
|
|
167
|
+
# echo 'my-strong-password' | drawlatch set-password # non-interactive
|
|
195
168
|
```
|
|
196
169
|
|
|
197
|
-
|
|
170
|
+
The password is hashed with **scrypt** (random 16-byte salt, verified with a constant-time compare); the hash + salt are written to `~/.drawlatch/.env` (`AUTH_PASSWORD_HASH` / `AUTH_PASSWORD_SALT`, mode `0600`). Your plaintext password is never stored. Use `drawlatch change-password` (an alias of the same command) to rotate it later — rotating signs out every other session.
|
|
198
171
|
|
|
199
|
-
|
|
172
|
+
**2. Open the dashboard** at `http://127.0.0.1:9999/` and log in. `drawlatch status` prints the dashboard URL and whether a password is configured.
|
|
200
173
|
|
|
201
|
-
|
|
174
|
+
### Pages
|
|
202
175
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
176
|
+
| Page | Route | What it shows | Refresh |
|
|
177
|
+
|------|-------|---------------|---------|
|
|
178
|
+
| **Overview** | `/` | Daemon health at a glance: status, PID, port, version, uptime, active session count, ingestor state breakdown, and secrets-configured progress. | on load |
|
|
179
|
+
| **Connections** | `/connections` | Full management: connections grouped by category with search + a stable/beta/dev filter; a caller selector with create/delete-caller; per-connection enable toggle, secrets modal, test connection/listener, listener config panel (all field types + multi-instance), and quick start/stop/restart — with live ingestor state dots and secret-status badges. | live (5s) |
|
|
180
|
+
| **Logs** | `/logs` | Live event/log feed per caller — ingestor status cards, source filter pills, and expandable event rows (eventType, ids, timestamps, pretty-printed JSON payload). | every 5s |
|
|
181
|
+
| **Callers** | `/callers` | Registered MCP callers — alias, name, connection count, key fingerprint, and whether their keys directory exists. Click through for a caller's connections and secret status. | on load |
|
|
182
|
+
| **Ingestors** | `/ingestors` | Live table of every running ingestor (WebSocket / webhook / poll) — state, buffered event count, total events received, last activity, and any error. | every 2s |
|
|
183
|
+
| **Sessions** | `/sessions` | Active MCP proxy sessions — caller alias, created/last-active times, request count, and current per-window request rate. | every 5s |
|
|
184
|
+
| **Secrets** | `/secrets` | A (caller × connection × secret) matrix showing required/optional and present/missing — with a "only missing" filter. **Never shows secret values**, only whether each is set. | every 10s |
|
|
206
185
|
|
|
207
|
-
|
|
186
|
+
### The `/api/admin/*` API
|
|
208
187
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"localKeyAlias": "my-laptop",
|
|
213
|
-
"remotePublicKeysDir": "~/.drawlatch/keys/peers/remote-server",
|
|
214
|
-
"connectTimeout": 10000,
|
|
215
|
-
"requestTimeout": 30000
|
|
216
|
-
}
|
|
217
|
-
```
|
|
188
|
+
The pages are views over the `/api/admin/*` JSON API. **Read** endpoints (`/meta`, `/health`, `/connections`, `/callers`, `/callers/:alias/connection-status`, `/callers/:alias/connections`, `/callers/:alias/ingestors`, `/callers/:alias/events`, `/ingestors`, `/sessions`, `/secrets`) **never return a secret value** — caller `env` maps are reduced to key *names*, secret state is reported as booleans, and session crypto material is never serialized.
|
|
189
|
+
|
|
190
|
+
**Mutating** endpoints (all behind the password gate) let the dashboard own management end-to-end:
|
|
218
191
|
|
|
219
|
-
|
|
|
220
|
-
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
192
|
+
| Method + path | Action |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `POST /callers` | Create a caller **with a fresh keypair** (no interactive sync) |
|
|
195
|
+
| `DELETE /callers/:alias` | Delete a caller (its keys + prefixed env vars); `default` is protected |
|
|
196
|
+
| `POST /callers/:alias/connections/:connection` `{enabled}` | Enable/disable a connection |
|
|
197
|
+
| `PUT /callers/:alias/connections/:connection/secrets` `{secrets}` | Set/clear secrets (empty string = delete) — **write-only**, read back as booleans |
|
|
198
|
+
| `POST /callers/:alias/connections/:connection/test` · `/test-ingestor` | Run a connection / listener test |
|
|
199
|
+
| `POST /callers/:alias/connections/:connection/listener/control` `{action,instance_id?}` | Start/stop/restart a listener |
|
|
200
|
+
| `GET/PUT /…/listener/params`, `GET/POST/DELETE /…/listener/instances[/:id]`, `POST /…/listener/resolve-options` | Listener params + multi-instance management |
|
|
227
201
|
|
|
228
|
-
**
|
|
202
|
+
Secrets are **write-only** through this API: you `PUT` values, and every read path reports only booleans. After any mutation the daemon live-reloads routes/ingestors for the affected caller. The same logic powers the encrypted MCP tools and the admin API through a single shared `tool-dispatch` module, so the two surfaces can never drift.
|
|
229
203
|
|
|
230
|
-
|
|
231
|
-
2. `localKeyAlias` in `proxy.config.json`
|
|
232
|
-
3. `localKeysDir` in `proxy.config.json` (explicit full path for custom deployments)
|
|
233
|
-
4. Default: `keys/local/default`
|
|
204
|
+
A loopback-only `POST /sync/auto-enroll` lets a **co-located** client (one that shares drawlatch's filesystem) provision a caller with zero interaction by presenting the one-time token drawlatch writes to `~/.drawlatch/enroll.token` at startup.
|
|
234
205
|
|
|
235
|
-
###
|
|
206
|
+
### Security model
|
|
236
207
|
|
|
237
|
-
|
|
208
|
+
The **password is the trust boundary** for the dashboard and `/api/admin/*` — not loopback. That lets you expose the dashboard to a LAN by binding a non-loopback host:
|
|
238
209
|
|
|
239
210
|
```bash
|
|
240
|
-
|
|
211
|
+
DRAWLATCH_HOST=0.0.0.0 drawlatch start # or: drawlatch start --host 0.0.0.0
|
|
241
212
|
```
|
|
242
213
|
|
|
243
|
-
|
|
214
|
+
Auth uses a `drawlatch_session` cookie that is `httpOnly` and `sameSite=strict`, with a **7-day rolling expiry** (every authenticated request extends it). Login, password-change, and auth-check endpoints are rate-limited per IP (5/min for login & change-password, 20/min for checks). If **no** password is configured, the daemon still starts and serves MCP normally — only the dashboard is locked: `/api/auth/*` and `/api/admin/*` return `503` and the SPA shows a locked state prompting `drawlatch set-password`. The daemon never exits just because the dashboard is unconfigured.
|
|
244
215
|
|
|
245
|
-
|
|
216
|
+
> **Cookies run over plain HTTP** on loopback/LAN (no `secure` flag). Put the daemon behind a TLS-terminating reverse proxy if you expose it beyond a trusted network.
|
|
246
217
|
|
|
247
|
-
|
|
218
|
+
> **Migrating from `drawlatch-ui`?** The standalone `drawlatch-ui` service and its `~/.drawlatch-ui/` config directory are abandoned — its dashboard, auth gate, and password now live inside drawlatch. There is no automatic migration: just run `drawlatch set-password` once to set the password in `~/.drawlatch/.env`.
|
|
219
|
+
|
|
220
|
+
## MCP Tools
|
|
221
|
+
|
|
222
|
+
Once connected, agents get these tools:
|
|
223
|
+
|
|
224
|
+
| Tool | Description |
|
|
225
|
+
|------|-------------|
|
|
226
|
+
| `secure_request` | Make authenticated HTTP requests. Route-level headers (auth tokens, API keys) are injected automatically — the agent never sees secret values. Supports JSON and multipart/form-data file uploads. |
|
|
227
|
+
| `list_routes` | Discover available APIs with metadata, docs links, allowed endpoints, and available secret placeholders. |
|
|
228
|
+
| `poll_events` | Retrieve buffered events from ingestors (Discord messages, GitHub webhooks, etc.) with cursor-based pagination. |
|
|
229
|
+
| `ingestor_status` | Get connection state, buffer sizes, event counts, and errors for all active ingestors. |
|
|
230
|
+
| `test_connection` | Verify API credentials with a pre-configured read-only request. |
|
|
231
|
+
| `control_listener` | Start, stop, or restart an event listener. |
|
|
232
|
+
| `list_listener_configs` | Get configurable fields for event listeners. |
|
|
233
|
+
| `set_listener_params` | Configure listener parameters (filters, buffer sizes, etc.). |
|
|
234
|
+
| `get_listener_params` | Read current listener parameter overrides. |
|
|
235
|
+
| `resolve_listener_options` | Fetch dynamic options for listener config fields (e.g., list of Trello boards). |
|
|
236
|
+
| `list_listener_instances` | List instances of a multi-instance listener. |
|
|
237
|
+
| `delete_listener_instance` | Remove a multi-instance listener instance. |
|
|
238
|
+
| `test_ingestor` | Test event listener configuration and credentials. |
|
|
239
|
+
|
|
240
|
+
## Configuration Reference
|
|
241
|
+
|
|
242
|
+
### Remote Server Config (`remote.config.json`)
|
|
248
243
|
|
|
249
244
|
```json
|
|
250
245
|
{
|
|
251
246
|
"host": "0.0.0.0",
|
|
252
247
|
"port": 9999,
|
|
253
|
-
"
|
|
254
|
-
"callers": {
|
|
255
|
-
"my-laptop": {
|
|
256
|
-
"name": "Personal Laptop",
|
|
257
|
-
"peerKeyDir": "~/.drawlatch/keys/peers/my-laptop",
|
|
258
|
-
"connections": ["github"]
|
|
259
|
-
}
|
|
260
|
-
},
|
|
248
|
+
"connectors": [],
|
|
249
|
+
"callers": {},
|
|
261
250
|
"rateLimitPerMinute": 60
|
|
262
251
|
}
|
|
263
252
|
```
|
|
264
253
|
|
|
265
|
-
|
|
254
|
+
| Field | Description | Default |
|
|
255
|
+
|-------|-------------|---------|
|
|
256
|
+
| `host` | Network interface to bind | `127.0.0.1` |
|
|
257
|
+
| `port` | Listen port | `9999` |
|
|
258
|
+
| `connectors` | Custom connector definitions (see below) | `[]` |
|
|
259
|
+
| `callers` | Per-caller access control (see below) | `{}` |
|
|
260
|
+
| `rateLimitPerMinute` | Max requests per minute per session | `60` |
|
|
261
|
+
|
|
262
|
+
Server keys are always loaded from `keys/server/` inside the config directory.
|
|
263
|
+
|
|
264
|
+
### Callers
|
|
266
265
|
|
|
267
|
-
|
|
266
|
+
Each caller is identified by their public key and declares which connections they can access:
|
|
268
267
|
|
|
269
268
|
```json
|
|
270
269
|
{
|
|
271
|
-
"host": "0.0.0.0",
|
|
272
|
-
"port": 9999,
|
|
273
|
-
"localKeysDir": "~/.drawlatch/keys/remote",
|
|
274
|
-
"connectors": [
|
|
275
|
-
{
|
|
276
|
-
"alias": "internal-api",
|
|
277
|
-
"name": "Internal Admin API",
|
|
278
|
-
"headers": { "Authorization": "Bearer ${ADMIN_KEY}" },
|
|
279
|
-
"secrets": { "ADMIN_KEY": "${INTERNAL_ADMIN_KEY}" },
|
|
280
|
-
"allowedEndpoints": ["https://admin.internal.com/**"]
|
|
281
|
-
}
|
|
282
|
-
],
|
|
283
270
|
"callers": {
|
|
284
271
|
"alice": {
|
|
285
272
|
"name": "Alice (senior engineer)",
|
|
286
|
-
"
|
|
287
|
-
"
|
|
273
|
+
"connections": ["github", "stripe", "internal-api"],
|
|
274
|
+
"env": {
|
|
275
|
+
"GITHUB_TOKEN": "${ALICE_GITHUB_TOKEN}"
|
|
276
|
+
}
|
|
288
277
|
},
|
|
289
278
|
"ci-server": {
|
|
290
279
|
"name": "GitHub Actions CI",
|
|
291
|
-
"peerKeyDir": "/keys/peers/ci-server",
|
|
292
280
|
"connections": ["github"]
|
|
293
281
|
}
|
|
294
|
-
}
|
|
295
|
-
"rateLimitPerMinute": 60
|
|
282
|
+
}
|
|
296
283
|
}
|
|
297
284
|
```
|
|
298
285
|
|
|
299
|
-
|
|
286
|
+
Caller public keys are loaded automatically from `keys/callers/<alias>/` — no path configuration needed.
|
|
287
|
+
|
|
288
|
+
| Field | Required | Description |
|
|
289
|
+
|-------|----------|-------------|
|
|
290
|
+
| `connections` | Yes | Array of connection names (built-in or custom connector aliases) |
|
|
291
|
+
| `name` | No | Human-readable name for audit logs |
|
|
292
|
+
| `env` | No | Per-caller env var overrides — redirect secret resolution per caller |
|
|
293
|
+
| `ingestorOverrides` | No | Per-caller ingestor config overrides ([details](INGESTORS.md#caller-level-ingestor-overrides)) |
|
|
294
|
+
|
|
295
|
+
The `env` map lets multiple callers share the same connection with different credentials:
|
|
296
|
+
- Keys are the env var names connectors reference (e.g., `GITHUB_TOKEN`)
|
|
297
|
+
- Values are `"${REAL_ENV_VAR}"` (redirect) or literal strings (direct injection)
|
|
298
|
+
- Checked before prefixed env vars during secret resolution
|
|
300
299
|
|
|
301
|
-
|
|
300
|
+
Without an explicit `env` mapping, secrets resolve via prefixed env vars (e.g., caller "alice" + `GITHUB_TOKEN` → `ALICE_GITHUB_TOKEN`).
|
|
302
301
|
|
|
303
|
-
|
|
302
|
+
### Custom Connectors
|
|
303
|
+
|
|
304
|
+
Define reusable route templates for APIs not covered by built-in connections:
|
|
304
305
|
|
|
305
306
|
```json
|
|
306
307
|
{
|
|
307
|
-
"
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"connections": ["github"],
|
|
315
|
-
"env": {
|
|
316
|
-
"GITHUB_TOKEN": "${ALICE_GITHUB_TOKEN}"
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
"bob": {
|
|
320
|
-
"name": "Bob",
|
|
321
|
-
"peerKeyDir": "/keys/peers/bob",
|
|
322
|
-
"connections": ["github", "stripe"],
|
|
323
|
-
"env": {
|
|
324
|
-
"GITHUB_TOKEN": "${BOB_GITHUB_TOKEN}",
|
|
325
|
-
"STRIPE_SECRET_KEY": "sk_test_bob_dev_key"
|
|
326
|
-
}
|
|
308
|
+
"connectors": [
|
|
309
|
+
{
|
|
310
|
+
"alias": "internal-api",
|
|
311
|
+
"name": "Internal Admin API",
|
|
312
|
+
"allowedEndpoints": ["https://admin.internal.com/**"],
|
|
313
|
+
"headers": { "Authorization": "Bearer ${ADMIN_KEY}" },
|
|
314
|
+
"secrets": { "ADMIN_KEY": "${INTERNAL_ADMIN_KEY}" }
|
|
327
315
|
}
|
|
328
|
-
|
|
329
|
-
"rateLimitPerMinute": 60
|
|
316
|
+
]
|
|
330
317
|
}
|
|
331
318
|
```
|
|
332
319
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
320
|
+
| Field | Required | Description |
|
|
321
|
+
|-------|----------|-------------|
|
|
322
|
+
| `alias` | Yes | Unique name for referencing from caller `connections` lists |
|
|
323
|
+
| `allowedEndpoints` | Yes | Glob patterns for allowed URLs |
|
|
324
|
+
| `name` | No | Human-readable name |
|
|
325
|
+
| `description` | No | Short description |
|
|
326
|
+
| `docsUrl` | No | URL to API documentation |
|
|
327
|
+
| `headers` | No | Headers to auto-inject (`${VAR}` placeholders resolved from `secrets`) |
|
|
328
|
+
| `secrets` | No | Key-value pairs — literal strings or `${ENV_VAR}` references |
|
|
329
|
+
| `resolveSecretsInBody` | No | Resolve `${VAR}` in request bodies (default: `false`) |
|
|
338
330
|
|
|
339
|
-
|
|
331
|
+
Custom connectors with an `alias` matching a built-in connection name take precedence.
|
|
340
332
|
|
|
341
|
-
|
|
333
|
+
### Proxy Config (`proxy.config.json`)
|
|
342
334
|
|
|
343
|
-
|
|
344
|
-
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
|
345
|
-
| `host` | Network interface to bind to. Use `0.0.0.0` for all interfaces or `127.0.0.1` for local only | `127.0.0.1` |
|
|
346
|
-
| `port` | Port to listen on | `9999` |
|
|
347
|
-
| `localKeysDir` | Absolute path to the remote server's own keypair | `~/.drawlatch/keys/remote` |
|
|
348
|
-
| `connectors` | Array of custom connector definitions, each with an `alias` for referencing from callers (see [Connector Definition](#connector-definition)) | `[]` |
|
|
349
|
-
| `callers` | Per-caller access control. Keys are caller aliases used in audit logs (see [Caller Definition](#caller-definition)) | `{}` |
|
|
350
|
-
| `rateLimitPerMinute` | Max requests per minute per session | `60` |
|
|
335
|
+
Used by the local MCP proxy to connect to the remote server:
|
|
351
336
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"remoteUrl": "http://127.0.0.1:9999",
|
|
340
|
+
"connectTimeout": 10000,
|
|
341
|
+
"requestTimeout": 30000
|
|
342
|
+
}
|
|
343
|
+
```
|
|
355
344
|
|
|
356
|
-
| Field
|
|
357
|
-
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
-
| `description` | No | Short description of what the connector provides |
|
|
362
|
-
| `docsUrl` | No | URL to API documentation |
|
|
363
|
-
| `openApiUrl` | No | URL to OpenAPI/Swagger spec |
|
|
364
|
-
| `headers` | No | Headers to auto-inject. Values may contain `${VAR}` placeholders resolved from `secrets` |
|
|
365
|
-
| `secrets` | No | Key-value pairs. Values can be literal strings or `${ENV_VAR}` references resolved from environment variables at startup |
|
|
366
|
-
| `resolveSecretsInBody` | No | Whether to resolve `${VAR}` placeholders in request bodies. Default: `false` |
|
|
345
|
+
| Field | Description | Default |
|
|
346
|
+
|-------|-------------|---------|
|
|
347
|
+
| `remoteUrl` | URL of the remote server | `http://localhost:9999` |
|
|
348
|
+
| `connectTimeout` | Handshake timeout (ms) | `10000` |
|
|
349
|
+
| `requestTimeout` | Request timeout (ms) | `30000` |
|
|
367
350
|
|
|
368
|
-
|
|
351
|
+
Key paths are derived automatically — no configuration needed:
|
|
352
|
+
- Caller keys: `keys/callers/{MCP_KEY_ALIAS || "default"}/`
|
|
353
|
+
- Server public keys: `keys/server/`
|
|
369
354
|
|
|
370
|
-
|
|
371
|
-
| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
372
|
-
| `peerKeyDir` | Yes | Path to this caller's public key files (`signing.pub.pem` + `exchange.pub.pem`) |
|
|
373
|
-
| `connections` | Yes | Array of connection names — references built-in templates (e.g., `"github"`) or custom connector aliases |
|
|
374
|
-
| `name` | No | Human-readable name for audit logs |
|
|
375
|
-
| `env` | No | Per-caller environment variable overrides (see [env overrides example](#example-per-caller-env-overrides-shared-connector-different-credentials)) |
|
|
376
|
-
| `ingestorOverrides` | No | Per-caller ingestor config overrides keyed by connection alias. Override event filters, buffer sizes, intents, or disable ingestors entirely. See **[INGESTORS.md](INGESTORS.md#caller-level-ingestor-overrides)** for full reference |
|
|
355
|
+
### Advanced Configuration
|
|
377
356
|
|
|
378
|
-
####
|
|
357
|
+
#### `MCP_CONFIG_DIR` — the config-dir contract
|
|
379
358
|
|
|
380
|
-
|
|
359
|
+
By default, all config and key files live in `~/.drawlatch/`. Override with:
|
|
381
360
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
361
|
+
```bash
|
|
362
|
+
export MCP_CONFIG_DIR=/custom/path/to/config
|
|
363
|
+
```
|
|
385
364
|
|
|
386
|
-
|
|
365
|
+
Useful for CI environments or running multiple independent setups on the same machine. drawlatch **owns this layout as a stable contract** (and migrates legacy key layouts into it automatically on startup):
|
|
387
366
|
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
367
|
+
```
|
|
368
|
+
$MCP_CONFIG_DIR/ (default: ~/.drawlatch)
|
|
369
|
+
remote.config.json — RemoteServerConfig (callers, connectors, port, tunnel flag)
|
|
370
|
+
proxy.config.json — ProxyConfig (local MCP proxy → remote URL)
|
|
371
|
+
.env — secret values, prefixed per caller (mode 0600)
|
|
372
|
+
enroll.token — one-time loopback auto-enroll token (mode 0600)
|
|
373
|
+
keys/
|
|
374
|
+
server/ — the daemon's own Ed25519 + X25519 keypair
|
|
375
|
+
callers/<alias>/ — one keypair per caller alias
|
|
392
376
|
```
|
|
393
377
|
|
|
394
|
-
|
|
378
|
+
Legacy `keys/local`, `keys/remote`, and `keys/peers/*` directories are migrated to `keys/callers` / `keys/server` on first start — idempotent and safe to re-run.
|
|
395
379
|
|
|
396
|
-
|
|
380
|
+
#### Self-managed tunnel
|
|
397
381
|
|
|
398
|
-
|
|
382
|
+
Set `"tunnel": true` in `remote.config.json` (or `DRAWLATCH_TUNNEL=1` / `drawlatch start --tunnel`) and drawlatch brings up and supervises its own Cloudflare quick tunnel on startup: it learns the public URL, injects it into callback-dependent connection configs (e.g. `TRELLO_CALLBACK_URL`) **before** secret resolution and ingestor start, and surfaces it in `drawlatch status`, the Overview page, and `/api/admin/meta`. It is a config flag, not a runtime control surface.
|
|
399
383
|
|
|
400
|
-
|
|
401
|
-
{
|
|
402
|
-
"callers": {
|
|
403
|
-
"my-laptop": {
|
|
404
|
-
"peerKeyDir": "/keys/peers/my-laptop",
|
|
405
|
-
"connections": ["github", "stripe"]
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
Set the required environment variables (e.g., `GITHUB_TOKEN`, `STRIPE_SECRET_KEY`) and the connection templates handle endpoint patterns, auth headers, docs URLs, and OpenAPI specs automatically. Custom connectors with a matching `alias` take precedence over built-in templates.
|
|
384
|
+
#### Daemon lifecycle
|
|
412
385
|
|
|
413
|
-
|
|
386
|
+
`drawlatch start` runs the **whole** daemon in one process (MCP protocol + admin API + dashboard UI + optional tunnel). It is daemon-first and cleanly supervisable: a PID file, a deterministic `start` / `stop` / `restart` / `status`, an unauthenticated `/health` endpoint, and `drawlatch start --foreground` for running under a process manager. Local and remote deployments differ only by host binding (`DRAWLATCH_HOST`) and the dashboard password.
|
|
414
387
|
|
|
415
|
-
|
|
388
|
+
## Connections
|
|
416
389
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
```bash
|
|
420
|
-
# Development (with hot reload via tsx)
|
|
421
|
-
npm run dev:remote
|
|
390
|
+
23 pre-built connection templates ship with drawlatch. Reference them by name in a caller's `connections` list:
|
|
422
391
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
392
|
+
| Connection | API | Required Env Var(s) |
|
|
393
|
+
|------------|-----|---------------------|
|
|
394
|
+
| `anthropic` | Anthropic Claude API | `ANTHROPIC_API_KEY` |
|
|
395
|
+
| `bluesky` | Bluesky (AT Protocol) | `BLUESKY_ACCESS_TOKEN` |
|
|
396
|
+
| `devin` | Devin AI API | `DEVIN_API_KEY` |
|
|
397
|
+
| `discord-bot` | Discord Bot API | `DISCORD_BOT_TOKEN` |
|
|
398
|
+
| `discord-oauth` | Discord OAuth2 API | `DISCORD_OAUTH_TOKEN` |
|
|
399
|
+
| `github` | GitHub REST API | `GITHUB_TOKEN` |
|
|
400
|
+
| `google` | Google Workspace APIs | `GOOGLE_API_TOKEN` |
|
|
401
|
+
| `google-ai` | Google AI (Gemini) | `GOOGLE_AI_API_KEY` |
|
|
402
|
+
| `hex` | Hex API | `HEX_TOKEN` |
|
|
403
|
+
| `lichess` | Lichess API | `LICHESS_API_TOKEN` |
|
|
404
|
+
| `linear` | Linear GraphQL API | `LINEAR_API_KEY` |
|
|
405
|
+
| `mastodon` | Mastodon API | `MASTODON_ACCESS_TOKEN` |
|
|
406
|
+
| `notion` | Notion API | `NOTION_API_KEY` |
|
|
407
|
+
| `openai` | OpenAI API | `OPENAI_API_KEY` |
|
|
408
|
+
| `openrouter` | OpenRouter API | `OPENROUTER_API_KEY` |
|
|
409
|
+
| `reddit` | Reddit API | `REDDIT_ACCESS_TOKEN` |
|
|
410
|
+
| `slack` | Slack Web API | `SLACK_BOT_TOKEN` |
|
|
411
|
+
| `stripe` | Stripe Payments API | `STRIPE_SECRET_KEY` |
|
|
412
|
+
| `telegram` | Telegram Bot API | `TELEGRAM_BOT_TOKEN` |
|
|
413
|
+
| `trello` | Trello API | `TRELLO_API_KEY`, `TRELLO_TOKEN` |
|
|
414
|
+
| `twitch` | Twitch Helix API | `TWITCH_ACCESS_TOKEN`, `TWITCH_CLIENT_ID` |
|
|
415
|
+
| `x` | X (Twitter) API v2 | `X_BEARER_TOKEN` |
|
|
426
416
|
|
|
427
|
-
**
|
|
417
|
+
See **[CONNECTIONS.md](CONNECTIONS.md)** for auth details, optional env vars, and usage notes per connection.
|
|
428
418
|
|
|
429
|
-
|
|
419
|
+
## Event Ingestion
|
|
430
420
|
|
|
431
|
-
|
|
421
|
+
Drawlatch can collect real-time events from external services and buffer them for agents to poll. Three ingestor types are supported:
|
|
432
422
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
423
|
+
| Type | How It Works | Connections |
|
|
424
|
+
|------|-------------|-------------|
|
|
425
|
+
| **WebSocket** | Persistent connections to event gateways | Discord Gateway, Slack Socket Mode |
|
|
426
|
+
| **Webhook** | HTTP receivers with signature verification | GitHub, Stripe, Trello |
|
|
427
|
+
| **Poll** | Interval-based HTTP requests | Notion, Linear, Reddit, X, Bluesky, Mastodon, Telegram, Twitch |
|
|
436
428
|
|
|
437
|
-
|
|
429
|
+
Events are stored in per-caller ring buffers (default 200, max 1000) with monotonic IDs for cursor-based pagination. Agents retrieve events via `poll_events` and check status via `ingestor_status`.
|
|
438
430
|
|
|
439
|
-
|
|
431
|
+
For webhook ingestors, the remote server must be publicly accessible (or behind a tunnel). Use `drawlatch start --tunnel` to automatically start a Cloudflare tunnel.
|
|
440
432
|
|
|
441
|
-
|
|
442
|
-
claude mcp add secure-proxy \
|
|
443
|
-
--transport stdio --scope local \
|
|
444
|
-
-e MCP_CONFIG_DIR=~/.drawlatch \
|
|
445
|
-
-- node /absolute/path/to/drawlatch/dist/mcp/server.js
|
|
446
|
-
```
|
|
433
|
+
See **[INGESTORS.md](INGESTORS.md)** for full configuration reference.
|
|
447
434
|
|
|
448
|
-
|
|
435
|
+
## Key Exchange
|
|
449
436
|
|
|
450
|
-
|
|
437
|
+
Remote mode requires mutual authentication via Ed25519/X25519 keypairs. Each identity gets four PEM files (signing + exchange, public + private). The `drawlatch init` command handles this automatically for single-machine setups.
|
|
451
438
|
|
|
452
|
-
|
|
439
|
+
**Directory structure:**
|
|
453
440
|
|
|
454
|
-
|
|
441
|
+
```
|
|
442
|
+
~/.drawlatch/keys/
|
|
443
|
+
├── callers/
|
|
444
|
+
│ ├── default/ # Default caller keypair
|
|
445
|
+
│ └── alice/ # Additional caller keypair
|
|
446
|
+
└── server/ # Server keypair
|
|
447
|
+
```
|
|
455
448
|
|
|
456
|
-
|
|
457
|
-
2. Point the external service's webhook URL to `https://<your-server>/webhooks/<path>` (e.g., `https://example.com/webhooks/github`)
|
|
458
|
-
3. Set the webhook signing secret as an environment variable on the remote server (e.g., `GITHUB_WEBHOOK_SECRET`, `STRIPE_WEBHOOK_SECRET`)
|
|
449
|
+
Both sides (caller and server) store their keys in the same directory tree. On a single machine, `drawlatch init` generates both and they can authenticate immediately. On separate machines, copy the `*.pub.pem` files to the corresponding directory on the other machine.
|
|
459
450
|
|
|
460
|
-
|
|
451
|
+
**Using [Callboard](https://github.com/WolpertingerLabs/callboard)?** Use `drawlatch sync` to exchange keys automatically via a double-code approval flow — no manual file copying needed.
|
|
461
452
|
|
|
462
|
-
### Multiple
|
|
453
|
+
### Multiple Agent Identities
|
|
463
454
|
|
|
464
|
-
|
|
455
|
+
Generate a keypair per agent and set `MCP_KEY_ALIAS` at spawn time:
|
|
465
456
|
|
|
466
457
|
```bash
|
|
467
|
-
|
|
468
|
-
|
|
458
|
+
drawlatch generate-keys caller alice
|
|
459
|
+
drawlatch generate-keys caller bob
|
|
469
460
|
```
|
|
470
461
|
|
|
471
|
-
Each agent's MCP server config specifies its alias via the `MCP_KEY_ALIAS` env var:
|
|
472
|
-
|
|
473
462
|
```json
|
|
474
463
|
{
|
|
475
464
|
"mcpServers": {
|
|
476
|
-
"
|
|
465
|
+
"drawlatch": {
|
|
477
466
|
"command": "node",
|
|
478
467
|
"args": ["dist/mcp/server.js"],
|
|
479
|
-
"env": {
|
|
480
|
-
"MCP_CONFIG_DIR": "~/.drawlatch",
|
|
481
|
-
"MCP_KEY_ALIAS": "alice"
|
|
482
|
-
}
|
|
468
|
+
"env": { "MCP_CONFIG_DIR": "~/.drawlatch", "MCP_KEY_ALIAS": "alice" }
|
|
483
469
|
}
|
|
484
470
|
}
|
|
485
471
|
}
|
|
486
472
|
```
|
|
487
473
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
## MCP Tools
|
|
491
|
-
|
|
492
|
-
Once connected, Claude Code gets access to four tools:
|
|
493
|
-
|
|
494
|
-
### `secure_request`
|
|
474
|
+
Register each agent as a separate caller in `remote.config.json`.
|
|
495
475
|
|
|
496
|
-
|
|
476
|
+
## CLI Reference
|
|
497
477
|
|
|
498
478
|
```
|
|
499
|
-
|
|
500
|
-
url: Full URL (may contain ${VAR} placeholders)
|
|
501
|
-
headers: Optional additional headers
|
|
502
|
-
body: Optional request body
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### `list_routes`
|
|
506
|
-
|
|
507
|
-
List all available routes for the current caller. Returns metadata (name, description, docs link), allowed endpoint patterns, available secret placeholder names (not values), and auto-injected header names. Different callers may see different routes based on their `connections` configuration.
|
|
508
|
-
|
|
509
|
-
### `poll_events`
|
|
510
|
-
|
|
511
|
-
Poll for new events from ingestors (Discord messages, GitHub webhooks, Notion updates, etc.). Returns events received since the given cursor.
|
|
512
|
-
|
|
513
|
-
```
|
|
514
|
-
connection: Optional — filter by connection alias (e.g., "discord-bot"), omit for all
|
|
515
|
-
after_id: Optional — cursor; returns events with id > after_id
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
Pass `after_id` from the last event you received to get only new events. Omit to get all buffered events. See **[INGESTORS.md](INGESTORS.md)** for details on configuring event sources.
|
|
519
|
-
|
|
520
|
-
### `ingestor_status`
|
|
479
|
+
drawlatch [command] [options]
|
|
521
480
|
|
|
522
|
-
|
|
481
|
+
Commands:
|
|
482
|
+
init Set up drawlatch (keys, config, .env) in one step
|
|
483
|
+
start Start the remote server (background daemon)
|
|
484
|
+
stop Stop the remote server
|
|
485
|
+
restart Restart the remote server
|
|
486
|
+
status Show server status (PID, port, uptime, health, sessions, dashboard URL)
|
|
487
|
+
logs View server logs
|
|
488
|
+
config Show effective configuration and secret status
|
|
489
|
+
doctor Validate setup and diagnose issues
|
|
490
|
+
set-password Set/change the dashboard password (alias: change-password)
|
|
491
|
+
generate-keys Generate Ed25519 + X25519 keypairs
|
|
492
|
+
sync Exchange keys with a callboard instance
|
|
523
493
|
|
|
524
|
-
|
|
494
|
+
Options:
|
|
495
|
+
-h, --help Show help
|
|
496
|
+
-v, --version Show version
|
|
525
497
|
|
|
526
|
-
|
|
498
|
+
Init options:
|
|
499
|
+
--connections <list> Comma-separated connections to enable (e.g., github,slack)
|
|
500
|
+
--alias <name> Caller alias (default: "default")
|
|
527
501
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
502
|
+
Start options:
|
|
503
|
+
-f, --foreground Run in foreground
|
|
504
|
+
-t, --tunnel Start a Cloudflare tunnel for webhooks
|
|
505
|
+
--port <number> Override configured port
|
|
506
|
+
--host <address> Override configured host
|
|
531
507
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
resolveCallerRoutes,
|
|
536
|
-
resolveRoutes,
|
|
537
|
-
resolveSecrets,
|
|
538
|
-
} from "drawlatch/shared/config";
|
|
508
|
+
Logs options:
|
|
509
|
+
-n, --lines <num> Number of lines (default: 50)
|
|
510
|
+
--follow Tail the log output
|
|
539
511
|
|
|
540
|
-
|
|
541
|
-
|
|
512
|
+
Generate-keys subcommands:
|
|
513
|
+
caller [alias] Generate caller keypair (default alias: "default")
|
|
514
|
+
server Generate server keypair
|
|
515
|
+
show <path> Show fingerprint of existing keypair
|
|
516
|
+
--dir <path> Generate to custom directory
|
|
542
517
|
|
|
543
|
-
|
|
544
|
-
|
|
518
|
+
Sync options:
|
|
519
|
+
--ttl <seconds> Session timeout (default: 300)
|
|
545
520
|
```
|
|
546
521
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
| Export Path | Description |
|
|
550
|
-
| ---------------------------- | ------------------------------------------------------------------ |
|
|
551
|
-
| `drawlatch` | MCP proxy server (stdio transport) — the default entry point |
|
|
552
|
-
| `drawlatch/remote/server` | Remote server functions including `executeProxyRequest()` |
|
|
553
|
-
| `drawlatch/remote/ingestors` | `IngestorManager` and all ingestor types |
|
|
554
|
-
| `drawlatch/shared/config` | Config loading, caller/route resolution, secret resolution |
|
|
555
|
-
| `drawlatch/shared/connections`| Connection template loading |
|
|
556
|
-
| `drawlatch/shared/crypto` | Key generation, encrypted channel, key serialization |
|
|
557
|
-
| `drawlatch/shared/protocol` | Handshake protocol, message types |
|
|
522
|
+
## Library Usage (Local Mode)
|
|
558
523
|
|
|
559
|
-
|
|
524
|
+
Import drawlatch directly for in-process use — no server, no encryption:
|
|
560
525
|
|
|
561
526
|
```typescript
|
|
562
527
|
import { loadRemoteConfig, resolveCallerRoutes, resolveRoutes, resolveSecrets } from "drawlatch/shared/config";
|
|
563
528
|
import { executeProxyRequest } from "drawlatch/remote/server";
|
|
564
529
|
|
|
565
|
-
// Load config and resolve routes for a specific caller
|
|
566
530
|
const config = loadRemoteConfig();
|
|
567
531
|
const callerRoutes = resolveCallerRoutes(config, "my-laptop");
|
|
568
532
|
const callerEnv = resolveSecrets(config.callers["my-laptop"]?.env ?? {});
|
|
569
533
|
const routes = resolveRoutes(callerRoutes, callerEnv);
|
|
570
534
|
|
|
571
|
-
// Make a request — same function the remote server uses
|
|
572
535
|
const result = await executeProxyRequest(
|
|
573
536
|
{ method: "GET", url: "https://api.github.com/user" },
|
|
574
537
|
routes,
|
|
575
538
|
);
|
|
576
539
|
```
|
|
577
540
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
## Development
|
|
541
|
+
### Available Exports
|
|
581
542
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
543
|
+
| Export Path | Description |
|
|
544
|
+
|-------------|-------------|
|
|
545
|
+
| `drawlatch` | MCP proxy server (stdio transport) |
|
|
546
|
+
| `drawlatch/remote/server` | `executeProxyRequest()` and server functions |
|
|
547
|
+
| `drawlatch/remote/ingestors` | `IngestorManager` and ingestor types |
|
|
548
|
+
| `drawlatch/shared/config` | Config loading, route/secret resolution |
|
|
549
|
+
| `drawlatch/shared/connections` | Connection template loading |
|
|
550
|
+
| `drawlatch/shared/env-utils` | Environment variable and secret utilities |
|
|
551
|
+
| `drawlatch/shared/crypto` | Key generation, encrypted channel |
|
|
552
|
+
| `drawlatch/shared/protocol` | Handshake protocol, message types |
|
|
585
553
|
|
|
586
|
-
|
|
587
|
-
npm run test:watch
|
|
554
|
+
## Security Model
|
|
588
555
|
|
|
589
|
-
|
|
590
|
-
npm run test:coverage
|
|
556
|
+
### Both Modes
|
|
591
557
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
558
|
+
- **Endpoint allowlisting** — requests only proxied to explicitly configured URL patterns
|
|
559
|
+
- **Per-caller access control** — each caller only sees their assigned connections
|
|
560
|
+
- **Per-caller credential isolation** — same connector, different credentials via `env` overrides
|
|
561
|
+
- **Rate limiting** — configurable per-session (default: 60/min)
|
|
562
|
+
- **Audit logging** — all operations logged with caller identity, session ID, timestamps
|
|
595
563
|
|
|
596
|
-
|
|
597
|
-
npm run format
|
|
598
|
-
npm run format:check
|
|
599
|
-
```
|
|
564
|
+
### Remote Mode Only
|
|
600
565
|
|
|
601
|
-
|
|
566
|
+
- **Zero secrets on the client** — the MCP proxy never sees API keys or tokens
|
|
567
|
+
- **Mutual authentication** — Ed25519 signatures before any data exchange
|
|
568
|
+
- **End-to-end encryption** — AES-256-GCM with X25519 ECDH session keys
|
|
569
|
+
- **Replay protection** — monotonic counters on all encrypted messages
|
|
570
|
+
- **Session isolation** — unique session keys per handshake, 30-minute TTL
|
|
571
|
+
- **File permissions** — private keys `0600`, key directories `0700`
|
|
602
572
|
|
|
603
|
-
|
|
573
|
+
## Development
|
|
604
574
|
|
|
605
|
-
|
|
575
|
+
```bash
|
|
576
|
+
npm test # Run tests
|
|
577
|
+
npm run test:watch # Watch mode
|
|
578
|
+
npm run test:coverage # Coverage report
|
|
579
|
+
npm run lint # Lint
|
|
580
|
+
npm run format # Format
|
|
606
581
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
├── .claude-plugin/ # Plugin metadata
|
|
610
|
-
│ ├── plugin.json # Plugin manifest (name, version, description)
|
|
611
|
-
│ └── marketplace.json # Marketplace catalog for distribution
|
|
612
|
-
├── .mcp.json # MCP server config (used by plugin system + auto-discovery)
|
|
613
|
-
├── dist/ # Compiled JavaScript (built via `npm run build` or `prepare`)
|
|
614
|
-
│ └── mcp/server.js # MCP proxy entrypoint
|
|
615
|
-
└── src/ # TypeScript source
|
|
582
|
+
npm run dev:remote # Remote server with hot reload
|
|
583
|
+
npm run dev:mcp # MCP proxy with hot reload
|
|
616
584
|
```
|
|
617
585
|
|
|
618
|
-
### Source
|
|
586
|
+
### Source Structure
|
|
619
587
|
|
|
620
588
|
```
|
|
621
589
|
src/
|
|
622
|
-
├── cli/
|
|
623
|
-
|
|
624
|
-
├──
|
|
625
|
-
|
|
626
|
-
│ ├── stripe.json # Stripe Payments API
|
|
627
|
-
│ └── ... # 22 templates total
|
|
628
|
-
├── mcp/
|
|
629
|
-
│ └── server.ts # Local MCP proxy server (stdio transport)
|
|
590
|
+
├── cli/ # Key generation CLI
|
|
591
|
+
├── connections/ # 23 pre-built route templates (JSON)
|
|
592
|
+
├── auth/ # Dashboard auth (scrypt password, session cookies)
|
|
593
|
+
├── mcp/server.ts # Local MCP proxy (stdio transport)
|
|
630
594
|
├── remote/
|
|
631
|
-
│ ├── server.ts
|
|
632
|
-
│ ├──
|
|
633
|
-
│
|
|
634
|
-
│
|
|
635
|
-
│ ├──
|
|
636
|
-
│ ├──
|
|
637
|
-
│
|
|
638
|
-
│ ├── registry.ts # Factory registry for ingestor types
|
|
639
|
-
│ ├── types.ts # Shared types and config interfaces
|
|
640
|
-
│ ├── discord/ # Discord Gateway WebSocket (v10)
|
|
641
|
-
│ ├── slack/ # Slack Socket Mode WebSocket
|
|
642
|
-
│ ├── webhook/ # Webhook receivers (GitHub, Stripe, Trello)
|
|
643
|
-
│ └── poll/ # Interval-based HTTP polling (Notion, Linear, etc.)
|
|
595
|
+
│ ├── server.ts # Remote secure server (Express) — also serves the dashboard
|
|
596
|
+
│ ├── admin.ts # Read-only /api/admin/* API
|
|
597
|
+
│ └── ingestors/ # Event ingestion system
|
|
598
|
+
│ ├── discord/ # Discord Gateway WebSocket
|
|
599
|
+
│ ├── slack/ # Slack Socket Mode WebSocket
|
|
600
|
+
│ ├── webhook/ # GitHub, Stripe, Trello webhooks
|
|
601
|
+
│ └── poll/ # Interval-based HTTP polling
|
|
644
602
|
└── shared/
|
|
645
|
-
├── config.ts
|
|
646
|
-
├── connections.ts
|
|
647
|
-
├──
|
|
648
|
-
├── crypto/
|
|
649
|
-
|
|
650
|
-
│ ├── channel.ts # AES-256-GCM encrypted channel
|
|
651
|
-
│ └── index.ts # Re-exports
|
|
652
|
-
└── protocol/
|
|
653
|
-
├── handshake.ts # Mutual auth (Noise NK-inspired)
|
|
654
|
-
├── messages.ts # Application-layer message types
|
|
655
|
-
└── index.ts # Re-exports
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
## Security Model
|
|
603
|
+
├── config.ts # Config loading, route resolution
|
|
604
|
+
├── connections.ts # Connection template loading
|
|
605
|
+
├── env-utils.ts # Environment variable utilities
|
|
606
|
+
├── crypto/ # Ed25519/X25519 keys, AES-256-GCM channel
|
|
607
|
+
└── protocol/ # Handshake, message types
|
|
659
608
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
- **Per-caller access control** — each caller only sees and can use the connections explicitly assigned to them
|
|
665
|
-
- **Per-caller credential isolation** — callers sharing the same connector can have different credentials via `env` overrides
|
|
666
|
-
- **Endpoint allowlisting** — requests are only proxied to explicitly configured URL patterns
|
|
667
|
-
- **Rate limiting** — configurable per-session request rate limiting (default: 60/min)
|
|
668
|
-
- **Audit logging** — all operations are logged with caller identity, session ID, and timestamps
|
|
669
|
-
|
|
670
|
-
### Remote Mode Only
|
|
671
|
-
|
|
672
|
-
These additional protections apply when running the two-component remote architecture:
|
|
673
|
-
|
|
674
|
-
- **Zero secrets on the client** — the local MCP proxy never sees API keys or tokens
|
|
675
|
-
- **Mutual authentication** — both sides prove their identity using Ed25519 signatures before any data is exchanged
|
|
676
|
-
- **End-to-end encryption** — all requests/responses are encrypted with AES-256-GCM session keys derived via X25519 ECDH
|
|
677
|
-
- **Replay protection** — monotonic counters prevent replay attacks
|
|
678
|
-
- **Session isolation** — each handshake produces unique session keys with a 30-minute TTL
|
|
679
|
-
- **File permissions** — private keys are saved with `0600`, directories with `0700`
|
|
680
|
-
|
|
681
|
-
### Local Mode Caveat
|
|
682
|
-
|
|
683
|
-
When using Drawlatch as an in-process library (local mode), secrets are resolved from `process.env` on the same machine as the agent. The encryption and mutual authentication layers are not used. The security value in local mode comes from **structured access control** (endpoint allowlisting, per-caller route isolation) rather than cryptographic secret isolation.
|
|
609
|
+
frontend/ # React + Vite dashboard SPA (built to frontend/dist)
|
|
610
|
+
└── src/pages/ # Overview, Connections, Callers, Ingestors, Sessions, Secrets
|
|
611
|
+
```
|
|
684
612
|
|
|
685
613
|
## License
|
|
686
614
|
|