@wolpertingerlabs/drawlatch 1.0.0-alpha.8.0 → 1.0.0-alpha.9.0

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/README.md CHANGED
@@ -1,89 +1,55 @@
1
1
  # Drawlatch
2
2
 
3
- > **Alpha Software:** This project is in alpha. Expect breaking changes between updates.
3
+ > **Alpha Software:** Expect breaking changes between updates.
4
4
 
5
- A config-driven MCP (Model Context Protocol) proxy that lets Claude Code make authenticated HTTP requests to external APIs. Supports 22 pre-built API connections with endpoint allowlisting, per-caller access control, and real-time event ingestion all configured through a single JSON file.
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 22 pre-built APIs without ever seeing your credentials.
6
6
 
7
- Drawlatch can run in two modes:
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
- - **Remote mode** — local proxy + remote server, with end-to-end encryption. Secrets never leave the remote server.
10
- - **Local mode** — imported as a library and called in-process (no server, no encryption). Secrets are on the same machine, but you get the same config-driven route resolution, endpoint allowlisting, and ingestor support.
9
+ ## Key Features
10
+
11
+ - **22 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
- ### Remote Mode (Two-Component)
19
+ Drawlatch runs in two modes depending on your trust model:
15
20
 
16
- In remote mode, the system has two components:
21
+ ### Remote Mode Secrets Never Leave the Server
17
22
 
18
- 1. **Local MCP Proxy** — runs on your machine as a Claude Code MCP server (stdio transport). It holds **no secrets**. It encrypts requests and forwards them to the remote server.
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
- ┌──────────────┐ Encrypted Channel ┌──────────────────┐ Authenticated ┌──────────────┐
23
- │ Claude Code │ ◄──── stdio ────► MCP │◄── HTTP + E2EE ──►│ Remote Server │──── HTTPS ───►│ External API │
24
- │ │ Proxy │ │ (holds secrets) │ │
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 **Ed25519** signatures for authentication and **X25519 ECDH** for key exchange, deriving **AES-256-GCM** session keys — all built on Node.js native `crypto` with zero external crypto dependencies.
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 (In-Process Library)
34
+ ### Local Mode In-Process Library
33
35
 
34
- In local mode, there is no separate server, no network port, and no encryption. Your application imports Drawlatch's core functions directly and calls them in-process:
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
- ┌──────────────────────────────────────────┐ Authenticated ┌──────────────┐
38
- │ Your Application │──── HTTPS ───────────►│ External API │
39
- │ ┌──────────┐ in-process ┌────────┐ │ │ │
40
- │ │ Agent │◄── call ────►│ drawl. │ │ Reads secrets from └──────────────┘
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
- **What you get in local mode:** The same config-driven route resolution, endpoint allowlisting, per-caller access control, connection templates, ingestor support (WebSocket, webhook, polling), and the exact same `executeProxyRequest()` function the remote server uses — no behavioral drift.
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 mode:** Use remote mode when you need to hide secrets from the machine running the agent (e.g., shared CI servers, untrusted environments). Use local mode when running on your own machine and you want the convenience of config-driven API management without the overhead of running a separate server.
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
- ### Option 1: Install as a Claude Code Plugin (Recommended)
56
-
57
- This repo is structured as a **Claude Code plugin** with a marketplace. Install it directly:
58
-
59
- ```shell
60
- # Add the marketplace (from a local clone)
61
- /plugin marketplace add ./path/to/drawlatch
62
-
63
- # Install the plugin
64
- /plugin install drawlatch@drawlatch
65
- ```
66
-
67
- Or load it directly during development:
68
-
69
- ```shell
70
- claude --plugin-dir ./path/to/drawlatch
71
- ```
72
-
73
- The plugin's MCP server starts automatically when enabled. The `secure_request` and `list_routes` tools become available immediately. The proxy uses `~/.drawlatch/` by default — see [Advanced Configuration](#advanced-configuration) to use a custom path.
74
-
75
- ### Option 2: Auto-Discovery (opening this repo directly)
76
-
77
- This repo includes a `.mcp.json` file at the root, so Claude Code **automatically discovers** the MCP proxy server when you open the project. On first launch, Claude Code will prompt you to approve the server — accept, and the `secure_request` and `list_routes` tools become available immediately.
78
-
79
- You need a working setup first (run `drawlatch init` + `drawlatch start`). See [Setup](#setup) below. The proxy uses `~/.drawlatch/` by default — see [Advanced Configuration](#advanced-configuration) to use a custom path.
80
-
81
- > **Note:** Auto-discovery uses the `dist/mcp/server.js` entrypoint. The `dist/` directory is built automatically when you run `npm install` (via the `prepare` script). If you need to rebuild manually, run `npm run build`.
82
-
83
- ## Setup
84
-
85
- ### Quick Start (Recommended)
86
-
87
53
  Get from zero to working in three commands:
88
54
 
89
55
  ```bash
@@ -108,630 +74,446 @@ drawlatch status # Check server is running
108
74
  drawlatch config # View configuration and secret status
109
75
  ```
110
76
 
111
- That's it. The `init` command generates keys, creates configs, exchanges public keys, and scaffolds the `.env` file. All steps are idempotent — safe to re-run.
112
-
113
- For custom setups (different aliases, multiple callers, different machines), see the [Manual Setup](#manual-setup) section below.
77
+ The `init` command generates keys, creates configs, exchanges public keys, and scaffolds the `.env` file. All steps are idempotent — safe to re-run.
114
78
 
115
- ### Manual Setup
79
+ ### Connect to Claude Code
116
80
 
117
- #### Prerequisites
81
+ **Option 1: Claude Code Plugin (Recommended)**
118
82
 
119
- ```bash
120
- git clone <repo-url>
121
- cd drawlatch
122
- npm install
123
- npm run build
83
+ ```shell
84
+ # Install the plugin
85
+ /plugin install drawlatch@drawlatch
124
86
  ```
125
87
 
126
- #### Directory Structure
127
-
128
- All config and key files live inside `~/.drawlatch/` in the user's home directory by default. Override with `MCP_CONFIG_DIR` for custom deployments (see [Advanced Configuration](#advanced-configuration)).
88
+ The plugin's MCP server starts automatically. The proxy uses `~/.drawlatch/` by default — see [Advanced Configuration](#advanced-configuration) to use a custom path.
129
89
 
130
- ```
131
- ~/.drawlatch/
132
- ├── proxy.config.json # Local proxy config
133
- ├── remote.config.json # Remote server config
134
- └── keys/
135
- ├── local/ # MCP proxy keypairs (one per alias)
136
- │ └── my-laptop/ # Alias-named subdirectory
137
- │ ├── signing.pub.pem # Ed25519 public key (share this)
138
- │ ├── signing.key.pem # Ed25519 private key (keep secret)
139
- │ ├── exchange.pub.pem # X25519 public key (share this)
140
- │ └── exchange.key.pem # X25519 private key (keep secret)
141
- ├── remote/ # Remote server keypair
142
- │ ├── signing.pub.pem
143
- │ ├── signing.key.pem
144
- │ ├── exchange.pub.pem
145
- │ └── exchange.key.pem
146
- └── peers/
147
- ├── alice/ # One subdirectory per caller
148
- │ ├── signing.pub.pem # Caller's public signing key
149
- │ └── exchange.pub.pem # Caller's public exchange key
150
- ├── bob/ # Another caller
151
- │ ├── signing.pub.pem
152
- │ └── exchange.pub.pem
153
- └── remote-server/ # Remote server's public keys (for proxy)
154
- ├── signing.pub.pem
155
- └── exchange.pub.pem
156
- ```
90
+ **Option 2: Auto-Discovery**
157
91
 
158
- #### Step 1: Generate Keys
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.
159
93
 
160
- Generate keypairs for both the local proxy and the remote server:
94
+ **Option 3: Manual Registration**
161
95
 
162
96
  ```bash
163
- # Generate local MCP proxy keypair (with alias)
164
- npm run generate-keys -- local my-laptop
165
-
166
- # Or use the default alias
167
- npm run generate-keys -- local
168
-
169
- # Generate remote server keypair
170
- npm run generate-keys -- remote
97
+ claude mcp add drawlatch \
98
+ -e MCP_CONFIG_DIR=~/.drawlatch \
99
+ -- node /path/to/drawlatch/dist/mcp/server.js
171
100
  ```
172
101
 
173
- Each command creates four PEM files (Ed25519 signing + X25519 exchange, public + private) in the appropriate directory under `~/.drawlatch/keys/`. Local keys are stored under `keys/local/<alias>/` the alias defaults to `"default"` if omitted.
174
-
175
- > **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.
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.
176
103
 
177
- You can also generate keys to a custom directory:
104
+ ### Manual Setup
178
105
 
179
- ```bash
180
- npm run generate-keys -- --dir /path/to/custom/keys
181
- ```
106
+ For custom setups (different aliases, multiple callers, different machines), you can configure everything manually instead of using `drawlatch init`.
182
107
 
183
- Or inspect the fingerprint of an existing keypair:
108
+ **1. Generate keys:**
184
109
 
185
110
  ```bash
186
- npm run generate-keys -- show ~/.drawlatch/keys/local/my-laptop
111
+ drawlatch generate-keys local my-laptop
112
+ drawlatch generate-keys remote
187
113
  ```
188
114
 
189
- #### Step 2: Exchange Public Keys
115
+ **2. Exchange public keys** — copy `*.pub.pem` files into the appropriate `keys/peers/` subdirectories. See [Key Exchange](#key-exchange) for details.
190
116
 
191
- The local proxy and remote server need each other's public keys for mutual authentication. Copy the **public** key files (`.pub.pem` only — never share private keys):
192
-
193
- **From local to remote** — copy the proxy's public keys into a caller directory on the remote server. Since local keys are now stored per-alias, the alias directory name naturally matches the peer directory:
117
+ **3. Create configs** copy the example files and edit:
194
118
 
195
119
  ```bash
196
- mkdir -p ~/.drawlatch/keys/peers/my-laptop
197
-
198
- cp ~/.drawlatch/keys/local/my-laptop/signing.pub.pem \
199
- ~/.drawlatch/keys/peers/my-laptop/signing.pub.pem
200
-
201
- cp ~/.drawlatch/keys/local/my-laptop/exchange.pub.pem \
202
- ~/.drawlatch/keys/peers/my-laptop/exchange.pub.pem
120
+ cp remote.config.example.json ~/.drawlatch/remote.config.json
121
+ cp proxy.config.example.json ~/.drawlatch/proxy.config.json
203
122
  ```
204
123
 
205
- **From remote to local** copy the remote server's public keys into the proxy's peer directory:
124
+ **4. Create a `.env` file** with your API secrets:
206
125
 
207
126
  ```bash
208
- mkdir -p ~/.drawlatch/keys/peers/remote-server
209
-
210
- cp ~/.drawlatch/keys/remote/signing.pub.pem \
211
- ~/.drawlatch/keys/peers/remote-server/signing.pub.pem
212
-
213
- cp ~/.drawlatch/keys/remote/exchange.pub.pem \
214
- ~/.drawlatch/keys/peers/remote-server/exchange.pub.pem
127
+ cat > ~/.drawlatch/.env << 'EOF'
128
+ # GITHUB_TOKEN=ghp_your_token_here
129
+ # DISCORD_BOT_TOKEN=your_bot_token_here
130
+ EOF
215
131
  ```
216
132
 
217
- > **Tip:** If the proxy and remote server are on different machines, securely transfer only the `*.pub.pem` files (e.g., via `scp`). Each caller gets its own subdirectory under the peers directory — the directory name becomes the caller's alias used in the remote config and audit logs.
218
-
219
- #### Step 3: Create the Local Proxy Config
220
-
221
- Copy the example and edit the paths to match your setup:
133
+ **5. Start the server:**
222
134
 
223
135
  ```bash
224
- cp proxy.config.example.json ~/.drawlatch/proxy.config.json
225
- ```
226
-
227
- Edit `~/.drawlatch/proxy.config.json`:
228
-
229
- ```json
230
- {
231
- "remoteUrl": "http://127.0.0.1:9999",
232
- "localKeyAlias": "my-laptop",
233
- "remotePublicKeysDir": "~/.drawlatch/keys/peers/remote-server",
234
- "connectTimeout": 10000,
235
- "requestTimeout": 30000
236
- }
136
+ drawlatch start
137
+ drawlatch doctor # Validate full setup
237
138
  ```
238
139
 
239
- | Field | Description | Default |
240
- | --------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------- |
241
- | `remoteUrl` | URL of the remote secure server | `http://localhost:9999` |
242
- | `localKeyAlias` | Key alias — resolved to `keys/local/<alias>/`. Overridden by `MCP_KEY_ALIAS` env var at runtime | _(none)_ |
243
- | `localKeysDir` | Absolute path to the proxy's own keypair directory. Ignored when `localKeyAlias` is set | `~/.drawlatch/keys/local/default` |
244
- | `remotePublicKeysDir` | Absolute path to the remote server's public keys | `~/.drawlatch/keys/peers/remote-server` |
245
- | `connectTimeout` | Handshake timeout in milliseconds | `10000` (10s) |
246
- | `requestTimeout` | Request timeout in milliseconds | `30000` (30s) |
247
-
248
- **Alias resolution priority:**
249
-
250
- 1. `MCP_KEY_ALIAS` env var (highest — set per agent at spawn time)
251
- 2. `localKeyAlias` in `proxy.config.json`
252
- 3. `localKeysDir` in `proxy.config.json` (explicit full path for custom deployments)
253
- 4. Default: `keys/local/default`
140
+ ## MCP Tools
254
141
 
255
- #### Step 4: Create the Remote Server Config
142
+ Once connected, agents get these tools:
256
143
 
257
- Copy the example and edit it to match your setup:
144
+ | Tool | Description |
145
+ |------|-------------|
146
+ | `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. |
147
+ | `list_routes` | Discover available APIs with metadata, docs links, allowed endpoints, and available secret placeholders. |
148
+ | `poll_events` | Retrieve buffered events from ingestors (Discord messages, GitHub webhooks, etc.) with cursor-based pagination. |
149
+ | `ingestor_status` | Get connection state, buffer sizes, event counts, and errors for all active ingestors. |
150
+ | `test_connection` | Verify API credentials with a pre-configured read-only request. |
151
+ | `control_listener` | Start, stop, or restart an event listener. |
152
+ | `list_listener_configs` | Get configurable fields for event listeners. |
153
+ | `set_listener_params` | Configure listener parameters (filters, buffer sizes, etc.). |
154
+ | `get_listener_params` | Read current listener parameter overrides. |
155
+ | `resolve_listener_options` | Fetch dynamic options for listener config fields (e.g., list of Trello boards). |
156
+ | `list_listener_instances` | List instances of a multi-instance listener. |
157
+ | `delete_listener_instance` | Remove a multi-instance listener instance. |
158
+ | `test_ingestor` | Test event listener configuration and credentials. |
258
159
 
259
- ```bash
260
- cp remote.config.example.json ~/.drawlatch/remote.config.json
261
- ```
160
+ ## Configuration Reference
262
161
 
263
- Edit `~/.drawlatch/remote.config.json`. This is where you define your callers, their connections, custom connectors, and secrets.
264
-
265
- The config is **caller-centric** — each caller is identified by their public key and explicitly declares which connections they can access.
266
-
267
- #### Example: Single caller with a built-in connection
162
+ ### Remote Server Config (`remote.config.json`)
268
163
 
269
164
  ```json
270
165
  {
271
166
  "host": "0.0.0.0",
272
167
  "port": 9999,
273
168
  "localKeysDir": "~/.drawlatch/keys/remote",
274
- "callers": {
275
- "my-laptop": {
276
- "name": "Personal Laptop",
277
- "peerKeyDir": "~/.drawlatch/keys/peers/my-laptop",
278
- "connections": ["github"]
279
- }
280
- },
169
+ "connectors": [],
170
+ "callers": {},
281
171
  "rateLimitPerMinute": 60
282
172
  }
283
173
  ```
284
174
 
285
- Set the `GITHUB_TOKEN` environment variable on the remote server and the built-in `github` connection template handles everything else — endpoint patterns, auth headers, docs URLs, and OpenAPI specs.
175
+ | Field | Description | Default |
176
+ |-------|-------------|---------|
177
+ | `host` | Network interface to bind | `127.0.0.1` |
178
+ | `port` | Listen port | `9999` |
179
+ | `localKeysDir` | Path to server's own keypair | `~/.drawlatch/keys/remote` |
180
+ | `connectors` | Custom connector definitions (see below) | `[]` |
181
+ | `callers` | Per-caller access control (see below) | `{}` |
182
+ | `rateLimitPerMinute` | Max requests per minute per session | `60` |
183
+
184
+ ### Callers
286
185
 
287
- #### Example: Multiple callers with different access levels
186
+ Each caller is identified by their public key and declares which connections they can access:
288
187
 
289
188
  ```json
290
189
  {
291
- "host": "0.0.0.0",
292
- "port": 9999,
293
- "localKeysDir": "~/.drawlatch/keys/remote",
294
- "connectors": [
295
- {
296
- "alias": "internal-api",
297
- "name": "Internal Admin API",
298
- "headers": { "Authorization": "Bearer ${ADMIN_KEY}" },
299
- "secrets": { "ADMIN_KEY": "${INTERNAL_ADMIN_KEY}" },
300
- "allowedEndpoints": ["https://admin.internal.com/**"]
301
- }
302
- ],
303
190
  "callers": {
304
191
  "alice": {
305
192
  "name": "Alice (senior engineer)",
306
- "peerKeyDir": "/keys/peers/alice",
307
- "connections": ["github", "stripe", "internal-api"]
193
+ "peerKeyDir": "~/.drawlatch/keys/peers/alice",
194
+ "connections": ["github", "stripe", "internal-api"],
195
+ "env": {
196
+ "GITHUB_TOKEN": "${ALICE_GITHUB_TOKEN}"
197
+ }
308
198
  },
309
199
  "ci-server": {
310
200
  "name": "GitHub Actions CI",
311
- "peerKeyDir": "/keys/peers/ci-server",
201
+ "peerKeyDir": "~/.drawlatch/keys/peers/ci-server",
312
202
  "connections": ["github"]
313
203
  }
314
- },
315
- "rateLimitPerMinute": 60
204
+ }
316
205
  }
317
206
  ```
318
207
 
319
- Alice gets access to GitHub, Stripe, and the internal API. The CI server only gets GitHub. Each caller is isolated — they only see the routes for their declared connections.
208
+ | Field | Required | Description |
209
+ |-------|----------|-------------|
210
+ | `peerKeyDir` | Yes | Path to this caller's public key files |
211
+ | `connections` | Yes | Array of connection names (built-in or custom connector aliases) |
212
+ | `name` | No | Human-readable name for audit logs |
213
+ | `env` | No | Per-caller env var overrides — redirect secret resolution per caller |
214
+ | `ingestorOverrides` | No | Per-caller ingestor config overrides ([details](INGESTORS.md#caller-level-ingestor-overrides)) |
215
+
216
+ The `env` map lets multiple callers share the same connection with different credentials:
217
+ - Keys are the env var names connectors reference (e.g., `GITHUB_TOKEN`)
218
+ - Values are `"${REAL_ENV_VAR}"` (redirect) or literal strings (direct injection)
219
+ - Checked before `process.env` during secret resolution
320
220
 
321
- #### Example: Per-caller env overrides (shared connector, different credentials)
221
+ ### Custom Connectors
322
222
 
323
- When multiple callers use the same connection but need different credentials, use the `env` field to redirect environment variable resolution per caller:
223
+ Define reusable route templates for APIs not covered by built-in connections:
324
224
 
325
225
  ```json
326
226
  {
327
- "host": "0.0.0.0",
328
- "port": 9999,
329
- "localKeysDir": "/keys/server",
330
- "callers": {
331
- "alice": {
332
- "name": "Alice",
333
- "peerKeyDir": "/keys/peers/alice",
334
- "connections": ["github"],
335
- "env": {
336
- "GITHUB_TOKEN": "${ALICE_GITHUB_TOKEN}"
337
- }
338
- },
339
- "bob": {
340
- "name": "Bob",
341
- "peerKeyDir": "/keys/peers/bob",
342
- "connections": ["github", "stripe"],
343
- "env": {
344
- "GITHUB_TOKEN": "${BOB_GITHUB_TOKEN}",
345
- "STRIPE_SECRET_KEY": "sk_test_bob_dev_key"
346
- }
227
+ "connectors": [
228
+ {
229
+ "alias": "internal-api",
230
+ "name": "Internal Admin API",
231
+ "allowedEndpoints": ["https://admin.internal.com/**"],
232
+ "headers": { "Authorization": "Bearer ${ADMIN_KEY}" },
233
+ "secrets": { "ADMIN_KEY": "${INTERNAL_ADMIN_KEY}" }
347
234
  }
348
- },
349
- "rateLimitPerMinute": 60
235
+ ]
350
236
  }
351
237
  ```
352
238
 
353
- The `env` map works as follows:
354
-
355
- - **Keys** are the env var names that connectors reference (e.g., `GITHUB_TOKEN`)
356
- - **Values** are either `"${REAL_ENV_VAR}"` (redirect to a different env var) or a literal string (direct injection)
357
- - When resolving secrets, the caller's `env` is checked **before** `process.env`
358
-
359
- In this example, both Alice and Bob use the same built-in `github` connection, but Alice's requests use `process.env.ALICE_GITHUB_TOKEN` while Bob's use `process.env.BOB_GITHUB_TOKEN`. Bob also gets a hardcoded Stripe test key without needing an env var.
360
-
361
- #### Remote Config Reference
362
-
363
- | Field | Description | Default |
364
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
365
- | `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` |
366
- | `port` | Port to listen on | `9999` |
367
- | `localKeysDir` | Absolute path to the remote server's own keypair | `~/.drawlatch/keys/remote` |
368
- | `connectors` | Array of custom connector definitions, each with an `alias` for referencing from callers (see [Connector Definition](#connector-definition)) | `[]` |
369
- | `callers` | Per-caller access control. Keys are caller aliases used in audit logs (see [Caller Definition](#caller-definition)) | `{}` |
370
- | `rateLimitPerMinute` | Max requests per minute per session | `60` |
371
-
372
- #### Connector Definition
373
-
374
- Custom connectors define reusable route templates referenced by `alias` from caller connection lists. They follow the same structure as routes:
375
-
376
- | Field | Required | Description |
377
- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
378
- | `alias` | Yes | Unique name for referencing this connector from caller `connections` lists |
379
- | `allowedEndpoints` | Yes | Array of glob patterns for allowed URLs (e.g., `https://api.example.com/**`) |
380
- | `name` | No | Human-readable name (e.g., `"Internal Admin API"`) |
381
- | `description` | No | Short description of what the connector provides |
382
- | `docsUrl` | No | URL to API documentation |
383
- | `openApiUrl` | No | URL to OpenAPI/Swagger spec |
384
- | `headers` | No | Headers to auto-inject. Values may contain `${VAR}` placeholders resolved from `secrets` |
385
- | `secrets` | No | Key-value pairs. Values can be literal strings or `${ENV_VAR}` references resolved from environment variables at startup |
386
- | `resolveSecretsInBody` | No | Whether to resolve `${VAR}` placeholders in request bodies. Default: `false` |
387
-
388
- #### Caller Definition
389
-
390
- | Field | Required | Description |
391
- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
392
- | `peerKeyDir` | Yes | Path to this caller's public key files (`signing.pub.pem` + `exchange.pub.pem`) |
393
- | `connections` | Yes | Array of connection names — references built-in templates (e.g., `"github"`) or custom connector aliases |
394
- | `name` | No | Human-readable name for audit logs |
395
- | `env` | No | Per-caller environment variable overrides (see [env overrides example](#example-per-caller-env-overrides-shared-connector-different-credentials)) |
396
- | `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 |
397
-
398
- #### How Secrets Work
239
+ | Field | Required | Description |
240
+ |-------|----------|-------------|
241
+ | `alias` | Yes | Unique name for referencing from caller `connections` lists |
242
+ | `allowedEndpoints` | Yes | Glob patterns for allowed URLs |
243
+ | `name` | No | Human-readable name |
244
+ | `description` | No | Short description |
245
+ | `docsUrl` | No | URL to API documentation |
246
+ | `headers` | No | Headers to auto-inject (`${VAR}` placeholders resolved from `secrets`) |
247
+ | `secrets` | No | Key-value pairs — literal strings or `${ENV_VAR}` references |
248
+ | `resolveSecretsInBody` | No | Resolve `${VAR}` in request bodies (default: `false`) |
399
249
 
400
- Secret values in the `secrets` map are resolved at session establishment time (per-caller):
250
+ Custom connectors with an `alias` matching a built-in connection name take precedence.
401
251
 
402
- - **Literal values** — used as-is: `"API_TOKEN": "sk_live_abc123"`
403
- - **Environment variable references** — resolved from the server's environment: `"API_TOKEN": "${API_TOKEN}"`
404
- - **Per-caller overrides** — when a caller has an `env` entry for a variable name, that value is used instead of `process.env`
252
+ ### Proxy Config (`proxy.config.json`)
405
253
 
406
- Header values can reference secrets using `${VAR}` placeholders:
407
-
408
- ```json
409
- "headers": {
410
- "Authorization": "Bearer ${API_TOKEN}"
411
- }
412
- ```
413
-
414
- The placeholder `${API_TOKEN}` is resolved against the route's resolved `secrets` map. This means the actual secret value is never exposed to the local proxy or Claude Code — it only exists on the remote server.
415
-
416
- #### Connections (Pre-built Route Templates)
417
-
418
- Instead of manually configuring connectors for popular APIs, you can use **connections** — pre-built route templates that ship with the package (`github`, `stripe`, `openai`, etc.). Reference them by name in a caller's `connections` list:
254
+ Used by the local MCP proxy to connect to the remote server:
419
255
 
420
256
  ```json
421
257
  {
422
- "callers": {
423
- "my-laptop": {
424
- "peerKeyDir": "/keys/peers/my-laptop",
425
- "connections": ["github", "stripe"]
426
- }
427
- }
258
+ "remoteUrl": "http://127.0.0.1:9999",
259
+ "localKeyAlias": "my-laptop",
260
+ "remotePublicKeysDir": "~/.drawlatch/keys/peers/remote-server",
261
+ "connectTimeout": 10000,
262
+ "requestTimeout": 30000
428
263
  }
429
264
  ```
430
265
 
431
- 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.
432
-
433
- See **[CONNECTIONS.md](CONNECTIONS.md)** for the full list of available connections, required environment variables, and usage examples.
434
-
435
- #### Step 5: Create an Environment File
266
+ | Field | Description | Default |
267
+ |-------|-------------|---------|
268
+ | `remoteUrl` | URL of the remote server | `http://localhost:9999` |
269
+ | `localKeyAlias` | Key alias — resolved to `keys/local/<alias>/` | _(none)_ |
270
+ | `localKeysDir` | Explicit path to proxy's keypair (ignored when `localKeyAlias` is set) | `~/.drawlatch/keys/local/default` |
271
+ | `remotePublicKeysDir` | Path to remote server's public keys | `~/.drawlatch/keys/peers/remote-server` |
272
+ | `connectTimeout` | Handshake timeout (ms) | `10000` |
273
+ | `requestTimeout` | Request timeout (ms) | `30000` |
436
274
 
437
- Create a `.env` file in your config directory with your API secrets:
275
+ Key alias resolution order: `MCP_KEY_ALIAS` env var > `localKeyAlias` > `localKeysDir` > `keys/local/default`.
438
276
 
439
- ```bash
440
- cat > ~/.drawlatch/.env << 'EOF'
441
- # Uncomment and set tokens for your enabled connections
442
- # GITHUB_TOKEN=ghp_your_token_here
443
- # DISCORD_BOT_TOKEN=your_bot_token_here
444
- # STRIPE_SECRET_KEY=sk_your_key_here
445
- EOF
446
- ```
447
-
448
- The remote server reads this file at startup. Use `drawlatch config` to check which secrets are set.
277
+ ### Advanced Configuration
449
278
 
450
- #### Step 6: Start the Servers
279
+ #### `MCP_CONFIG_DIR`
451
280
 
452
- **Start the remote server:**
281
+ By default, all config and key files live in `~/.drawlatch/`. Override with:
453
282
 
454
283
  ```bash
455
- # Using the drawlatch CLI (recommended — runs as a background daemon)
456
- drawlatch start
457
-
458
- # Or for development (with hot reload via tsx)
459
- npm run dev:remote
460
-
461
- # Or production without the daemon (requires `npm run build` first)
462
- npm run start:remote
284
+ export MCP_CONFIG_DIR=/custom/path/to/config
463
285
  ```
464
286
 
465
- Verify it's running:
287
+ Useful for CI environments or running multiple independent setups on the same machine.
466
288
 
467
- ```bash
468
- drawlatch status # Check PID, uptime, health
469
- drawlatch doctor # Validate full setup
470
- ```
289
+ ## Connections
471
290
 
472
- **Connect the local MCP proxy to Claude Code:**
291
+ 22 pre-built connection templates ship with drawlatch. Reference them by name in a caller's `connections` list:
473
292
 
474
- The repo includes a `.mcp.json` at the root, so Claude Code auto-discovers the proxy when you open the project directory. Just approve the server when prompted — no manual registration needed. The proxy uses `~/.drawlatch/` by default.
293
+ | Connection | API | Required Env Var(s) |
294
+ |------------|-----|---------------------|
295
+ | `anthropic` | Anthropic Claude API | `ANTHROPIC_API_KEY` |
296
+ | `bluesky` | Bluesky (AT Protocol) | `BLUESKY_ACCESS_TOKEN` |
297
+ | `devin` | Devin AI API | `DEVIN_API_KEY` |
298
+ | `discord-bot` | Discord Bot API | `DISCORD_BOT_TOKEN` |
299
+ | `discord-oauth` | Discord OAuth2 API | `DISCORD_OAUTH_TOKEN` |
300
+ | `github` | GitHub REST API | `GITHUB_TOKEN` |
301
+ | `google` | Google Workspace APIs | `GOOGLE_API_TOKEN` |
302
+ | `google-ai` | Google AI (Gemini) | `GOOGLE_AI_API_KEY` |
303
+ | `hex` | Hex API | `HEX_TOKEN` |
304
+ | `lichess` | Lichess API | `LICHESS_API_TOKEN` |
305
+ | `linear` | Linear GraphQL API | `LINEAR_API_KEY` |
306
+ | `mastodon` | Mastodon API | `MASTODON_ACCESS_TOKEN` |
307
+ | `notion` | Notion API | `NOTION_API_KEY` |
308
+ | `openai` | OpenAI API | `OPENAI_API_KEY` |
309
+ | `openrouter` | OpenRouter API | `OPENROUTER_API_KEY` |
310
+ | `reddit` | Reddit API | `REDDIT_ACCESS_TOKEN` |
311
+ | `slack` | Slack Web API | `SLACK_BOT_TOKEN` |
312
+ | `stripe` | Stripe Payments API | `STRIPE_SECRET_KEY` |
313
+ | `telegram` | Telegram Bot API | `TELEGRAM_BOT_TOKEN` |
314
+ | `trello` | Trello API | `TRELLO_API_KEY`, `TRELLO_TOKEN` |
315
+ | `twitch` | Twitch Helix API | `TWITCH_ACCESS_TOKEN`, `TWITCH_CLIENT_ID` |
316
+ | `x` | X (Twitter) API v2 | `X_BEARER_TOKEN` |
475
317
 
476
- **Alternative: manual registration**
318
+ See **[CONNECTIONS.md](CONNECTIONS.md)** for auth details, optional env vars, and usage notes per connection.
477
319
 
478
- If you prefer not to use auto-discovery, register the MCP server directly:
320
+ ## Event Ingestion
479
321
 
480
- ```bash
481
- claude mcp add secure-proxy \
482
- --transport stdio --scope local \
483
- -- node /absolute/path/to/drawlatch/dist/mcp/server.js
484
- ```
322
+ Drawlatch can collect real-time events from external services and buffer them for agents to poll. Three ingestor types are supported:
485
323
 
486
- After connecting (either via auto-discovery or manual registration), the proxy will automatically perform the encrypted handshake with the remote server on first use.
324
+ | Type | How It Works | Connections |
325
+ |------|-------------|-------------|
326
+ | **WebSocket** | Persistent connections to event gateways | Discord Gateway, Slack Socket Mode |
327
+ | **Webhook** | HTTP receivers with signature verification | GitHub, Stripe, Trello |
328
+ | **Poll** | Interval-based HTTP requests | Notion, Linear, Reddit, X, Bluesky, Mastodon, Telegram, Twitch |
487
329
 
488
- #### Step 7: Webhook Endpoints (Optional)
330
+ 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`.
489
331
 
490
- If any of your connections use webhook ingestors (e.g., GitHub, Stripe, Trello), the remote server automatically exposes `POST /webhooks/:path` routes on the same port. External services send webhook POSTs to these endpoints, and the server verifies signatures, buffers events in per-caller ring buffers, and makes them available via `poll_events`.
332
+ For webhook ingestors, the remote server must be publicly accessible (or behind a tunnel). Use `drawlatch start --tunnel` to automatically start a Cloudflare tunnel.
491
333
 
492
- **Setup:**
334
+ See **[INGESTORS.md](INGESTORS.md)** for full configuration reference.
493
335
 
494
- 1. The remote server must be **publicly accessible** for webhook delivery (or behind a tunnel like [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/))
495
- 2. Point the external service's webhook URL to `https://<your-server>/webhooks/<path>` (e.g., `https://example.com/webhooks/github`)
496
- 3. Set the webhook signing secret as an environment variable on the remote server (e.g., `GITHUB_WEBHOOK_SECRET`, `STRIPE_WEBHOOK_SECRET`)
336
+ ## Key Exchange
497
337
 
498
- The webhook path is configured in each connection template's `ingestor.webhook.path` field. See **[INGESTORS.md](INGESTORS.md)** for full details on webhook, WebSocket, and poll ingestors.
338
+ 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.
499
339
 
500
- ### Advanced Configuration
340
+ For multi-machine setups, exchange public keys manually:
501
341
 
502
- #### `MCP_CONFIG_DIR`
342
+ **Directory structure:**
343
+
344
+ ```
345
+ ~/.drawlatch/keys/
346
+ ├── local/my-laptop/ # MCP proxy keypair
347
+ ├── remote/ # Remote server keypair
348
+ └── peers/
349
+ ├── my-laptop/ # Proxy's public keys (on the server)
350
+ └── remote-server/ # Server's public keys (on the proxy)
351
+ ```
503
352
 
504
- By default, all config and key files live in `~/.drawlatch/`. You can override this by setting the `MCP_CONFIG_DIR` environment variable before launching the proxy or server:
353
+ **Exchange public keys** (`.pub.pem` only never share private keys):
505
354
 
506
355
  ```bash
507
- export MCP_CONFIG_DIR=/custom/path/to/config
356
+ # Proxy's public keys → server's peers directory
357
+ cp keys/local/my-laptop/signing.pub.pem keys/peers/my-laptop/signing.pub.pem
358
+ cp keys/local/my-laptop/exchange.pub.pem keys/peers/my-laptop/exchange.pub.pem
359
+
360
+ # Server's public keys → proxy's peers directory
361
+ cp keys/remote/signing.pub.pem keys/peers/remote-server/signing.pub.pem
362
+ cp keys/remote/exchange.pub.pem keys/peers/remote-server/exchange.pub.pem
508
363
  ```
509
364
 
510
- This is useful for non-standard deployments, CI environments, or running multiple independent setups on the same machine.
365
+ If the proxy and server are on different machines, transfer only `*.pub.pem` files via `scp` or similar.
511
366
 
512
- #### Multiple Agents (Multi-Identity)
367
+ ### Multiple Agent Identities
513
368
 
514
- When multiple agents share the same machine, each needs its own key identity. Generate a keypair per agent:
369
+ Generate a keypair per agent and set `MCP_KEY_ALIAS` at spawn time:
515
370
 
516
371
  ```bash
517
- npm run generate-keys -- local alice
518
- npm run generate-keys -- local bob
372
+ drawlatch generate-keys local alice
373
+ drawlatch generate-keys local bob
519
374
  ```
520
375
 
521
- Each agent's MCP server config specifies its alias via the `MCP_KEY_ALIAS` env var:
522
-
523
376
  ```json
524
377
  {
525
378
  "mcpServers": {
526
- "secure-proxy": {
379
+ "drawlatch": {
527
380
  "command": "node",
528
381
  "args": ["dist/mcp/server.js"],
529
- "env": {
530
- "MCP_CONFIG_DIR": "~/.drawlatch",
531
- "MCP_KEY_ALIAS": "alice"
532
- }
382
+ "env": { "MCP_CONFIG_DIR": "~/.drawlatch", "MCP_KEY_ALIAS": "alice" }
533
383
  }
534
384
  }
535
385
  }
536
386
  ```
537
387
 
538
- The proxy auto-resolves `MCP_KEY_ALIAS=alice` to `keys/local/alice/`. On the remote server, register each agent as a separate caller with matching alias directories under `keys/peers/`.
539
-
540
- ## MCP Tools
541
-
542
- Once connected, Claude Code gets access to four tools:
543
-
544
- ### `secure_request`
545
-
546
- Make an authenticated HTTP request through the proxy. Route-level headers (e.g., `Authorization`) are injected automatically — the agent never sees the secret values.
547
-
548
- ```
549
- method: GET | POST | PUT | PATCH | DELETE
550
- url: Full URL (may contain ${VAR} placeholders)
551
- headers: Optional additional headers
552
- body: Optional request body
553
- ```
554
-
555
- ### `list_routes`
556
-
557
- 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.
388
+ Register each agent as a separate caller on the remote server with matching peer key directories.
558
389
 
559
- ### `poll_events`
390
+ ## CLI Reference
560
391
 
561
- Poll for new events from ingestors (Discord messages, GitHub webhooks, Notion updates, etc.). Returns events received since the given cursor.
562
-
563
- ```
564
- connection: Optional — filter by connection alias (e.g., "discord-bot"), omit for all
565
- after_id: Optional — cursor; returns events with id > after_id
566
392
  ```
393
+ drawlatch [command] [options]
567
394
 
568
- 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.
395
+ Commands:
396
+ init Set up drawlatch (keys, config, .env) in one step
397
+ start Start the remote server (background daemon)
398
+ stop Stop the remote server
399
+ restart Restart the remote server
400
+ status Show server status (PID, port, uptime, health, sessions)
401
+ logs View server logs
402
+ config Show effective configuration and secret status
403
+ doctor Validate setup and diagnose issues
404
+ generate-keys Generate Ed25519 + X25519 keypairs
569
405
 
570
- ### `ingestor_status`
406
+ Options:
407
+ -h, --help Show help
408
+ -v, --version Show version
571
409
 
572
- Get the status of all active ingestors for the current caller. Returns connection state, buffer sizes, event counts, and any errors. Takes no parameters.
410
+ Init options:
411
+ --connections <list> Comma-separated connections to enable (e.g., github,slack)
412
+ --alias <name> Caller alias (default: "default")
573
413
 
574
- ## Library Usage (Local Mode)
414
+ Start options:
415
+ -f, --foreground Run in foreground
416
+ -t, --tunnel Start a Cloudflare tunnel for webhooks
417
+ --port <number> Override configured port
418
+ --host <address> Override configured host
575
419
 
576
- Drawlatch can be imported as a library for in-process use — no separate server, no encryption overhead. The `package.json` exports map provides clean entry points:
420
+ Logs options:
421
+ -n, --lines <num> Number of lines (default: 50)
422
+ --follow Tail the log output
577
423
 
578
- ```typescript
579
- // Core request execution (same function the remote server uses)
580
- import { executeProxyRequest } from "drawlatch/remote/server";
581
-
582
- // Config loading and route resolution
583
- import {
584
- loadRemoteConfig,
585
- resolveCallerRoutes,
586
- resolveRoutes,
587
- resolveSecrets,
588
- } from "drawlatch/shared/config";
589
-
590
- // Ingestor management (WebSocket, webhook, poll)
591
- import { IngestorManager } from "drawlatch/remote/ingestors";
592
-
593
- // Crypto primitives (if building custom transport)
594
- import { loadKeyBundle, loadPublicKeys, EncryptedChannel } from "drawlatch/shared/crypto";
424
+ Generate-keys subcommands:
425
+ local [alias] Generate local proxy keypair (default alias: "default")
426
+ remote Generate remote server keypair
427
+ show <path> Show fingerprint of existing keypair
428
+ --dir <path> Generate to custom directory
595
429
  ```
596
430
 
597
- ### Available Exports
598
-
599
- | Export Path | Description |
600
- | ---------------------------- | ------------------------------------------------------------------ |
601
- | `drawlatch` | MCP proxy server (stdio transport) — the default entry point |
602
- | `drawlatch/remote/server` | Remote server functions including `executeProxyRequest()` |
603
- | `drawlatch/remote/ingestors` | `IngestorManager` and all ingestor types |
604
- | `drawlatch/shared/config` | Config loading, caller/route resolution, secret resolution |
605
- | `drawlatch/shared/connections`| Connection template loading |
606
- | `drawlatch/shared/crypto` | Key generation, encrypted channel, key serialization |
607
- | `drawlatch/shared/protocol` | Handshake protocol, message types |
431
+ ## Library Usage (Local Mode)
608
432
 
609
- ### Example: In-Process Proxy
433
+ Import drawlatch directly for in-process use — no server, no encryption:
610
434
 
611
435
  ```typescript
612
436
  import { loadRemoteConfig, resolveCallerRoutes, resolveRoutes, resolveSecrets } from "drawlatch/shared/config";
613
437
  import { executeProxyRequest } from "drawlatch/remote/server";
614
438
 
615
- // Load config and resolve routes for a specific caller
616
439
  const config = loadRemoteConfig();
617
440
  const callerRoutes = resolveCallerRoutes(config, "my-laptop");
618
441
  const callerEnv = resolveSecrets(config.callers["my-laptop"]?.env ?? {});
619
442
  const routes = resolveRoutes(callerRoutes, callerEnv);
620
443
 
621
- // Make a request — same function the remote server uses
622
444
  const result = await executeProxyRequest(
623
445
  { method: "GET", url: "https://api.github.com/user" },
624
446
  routes,
625
447
  );
626
448
  ```
627
449
 
628
- > **Note:** In local mode, secrets are resolved from `process.env` on the same machine. The encryption layer is not used. See [How It Works → Local Mode](#local-mode-in-process-library) for the security tradeoff.
629
-
630
- ## Development
450
+ ### Available Exports
631
451
 
632
- ```bash
633
- # Run tests
634
- npm test
452
+ | Export Path | Description |
453
+ |-------------|-------------|
454
+ | `drawlatch` | MCP proxy server (stdio transport) |
455
+ | `drawlatch/remote/server` | `executeProxyRequest()` and server functions |
456
+ | `drawlatch/remote/ingestors` | `IngestorManager` and ingestor types |
457
+ | `drawlatch/shared/config` | Config loading, route/secret resolution |
458
+ | `drawlatch/shared/connections` | Connection template loading |
459
+ | `drawlatch/shared/env-utils` | Environment variable and secret utilities |
460
+ | `drawlatch/shared/crypto` | Key generation, encrypted channel |
461
+ | `drawlatch/shared/protocol` | Handshake protocol, message types |
635
462
 
636
- # Run tests in watch mode
637
- npm run test:watch
463
+ ## Security Model
638
464
 
639
- # Run tests with coverage
640
- npm run test:coverage
465
+ ### Both Modes
641
466
 
642
- # Lint
643
- npm run lint
644
- npm run lint:fix
467
+ - **Endpoint allowlisting** — requests only proxied to explicitly configured URL patterns
468
+ - **Per-caller access control** — each caller only sees their assigned connections
469
+ - **Per-caller credential isolation** — same connector, different credentials via `env` overrides
470
+ - **Rate limiting** — configurable per-session (default: 60/min)
471
+ - **Audit logging** — all operations logged with caller identity, session ID, timestamps
645
472
 
646
- # Format
647
- npm run format
648
- npm run format:check
649
- ```
473
+ ### Remote Mode Only
650
474
 
651
- ## Architecture
475
+ - **Zero secrets on the client** — the MCP proxy never sees API keys or tokens
476
+ - **Mutual authentication** — Ed25519 signatures before any data exchange
477
+ - **End-to-end encryption** — AES-256-GCM with X25519 ECDH session keys
478
+ - **Replay protection** — monotonic counters on all encrypted messages
479
+ - **Session isolation** — unique session keys per handshake, 30-minute TTL
480
+ - **File permissions** — private keys `0600`, key directories `0700`
652
481
 
653
- ### Plugin Structure
482
+ ## Development
654
483
 
655
- This repo is structured as a Claude Code plugin:
484
+ ```bash
485
+ npm test # Run tests
486
+ npm run test:watch # Watch mode
487
+ npm run test:coverage # Coverage report
488
+ npm run lint # Lint
489
+ npm run format # Format
656
490
 
657
- ```
658
- drawlatch/
659
- ├── .claude-plugin/ # Plugin metadata
660
- │ ├── plugin.json # Plugin manifest (name, version, description)
661
- │ └── marketplace.json # Marketplace catalog for distribution
662
- ├── .mcp.json # MCP server config (used by plugin system + auto-discovery)
663
- ├── dist/ # Compiled JavaScript (built via `npm run build` or `prepare`)
664
- │ └── mcp/server.js # MCP proxy entrypoint
665
- └── src/ # TypeScript source
491
+ npm run dev:remote # Remote server with hot reload
492
+ npm run dev:mcp # MCP proxy with hot reload
666
493
  ```
667
494
 
668
- ### Source Code
495
+ ### Source Structure
669
496
 
670
497
  ```
671
498
  src/
672
- ├── cli/ # Key generation CLI
673
- │ └── generate-keys.ts # Ed25519 + X25519 keypair generation
674
- ├── connections/ # Pre-built route templates (JSON)
675
- │ ├── github.json # GitHub REST API
676
- │ ├── stripe.json # Stripe Payments API
677
- │ └── ... # 22 templates total
678
- ├── mcp/
679
- │ └── server.ts # Local MCP proxy server (stdio transport)
499
+ ├── cli/ # Key generation CLI
500
+ ├── connections/ # 22 pre-built route templates (JSON)
501
+ ├── mcp/server.ts # Local MCP proxy (stdio transport)
680
502
  ├── remote/
681
- │ ├── server.ts # Remote secure server (Express HTTP)
682
- ├── server.test.ts # Unit tests
683
- ├── server.e2e.test.ts # End-to-end tests
684
- └── ingestors/ # Real-time event ingestion system
685
- │ ├── base-ingestor.ts # Abstract base class (state machine, ring buffer)
686
- ├── ring-buffer.ts # Generic bounded circular buffer
687
- │ ├── manager.ts # Lifecycle management, per-caller routing
688
- │ ├── registry.ts # Factory registry for ingestor types
689
- │ ├── types.ts # Shared types and config interfaces
690
- │ ├── discord/ # Discord Gateway WebSocket (v10)
691
- │ ├── slack/ # Slack Socket Mode WebSocket
692
- │ ├── webhook/ # Webhook receivers (GitHub, Stripe, Trello)
693
- │ └── poll/ # Interval-based HTTP polling (Notion, Linear, etc.)
503
+ │ ├── server.ts # Remote secure server (Express)
504
+ └── ingestors/ # Event ingestion system
505
+ ├── discord/ # Discord Gateway WebSocket
506
+ ├── slack/ # Slack Socket Mode WebSocket
507
+ │ ├── webhook/ # GitHub, Stripe, Trello webhooks
508
+ └── poll/ # Interval-based HTTP polling
694
509
  └── shared/
695
- ├── config.ts # Config loading/saving, caller & route resolution
696
- ├── connections.ts # Connection template loading
697
- ├── logger.ts # Structured logging
698
- ├── crypto/
699
- │ ├── keys.ts # Ed25519 + X25519 key generation/serialization
700
- │ ├── channel.ts # AES-256-GCM encrypted channel
701
- │ └── index.ts # Re-exports
702
- └── protocol/
703
- ├── handshake.ts # Mutual auth (Noise NK-inspired)
704
- ├── messages.ts # Application-layer message types
705
- └── index.ts # Re-exports
510
+ ├── config.ts # Config loading, route resolution
511
+ ├── connections.ts # Connection template loading
512
+ ├── env-utils.ts # Environment variable utilities
513
+ ├── crypto/ # Ed25519/X25519 keys, AES-256-GCM channel
514
+ └── protocol/ # Handshake, message types
706
515
  ```
707
516
 
708
- ## Security Model
709
-
710
- ### Both Modes
711
-
712
- These protections apply regardless of whether you use remote or local mode:
713
-
714
- - **Per-caller access control** — each caller only sees and can use the connections explicitly assigned to them
715
- - **Per-caller credential isolation** — callers sharing the same connector can have different credentials via `env` overrides
716
- - **Endpoint allowlisting** — requests are only proxied to explicitly configured URL patterns
717
- - **Rate limiting** — configurable per-session request rate limiting (default: 60/min)
718
- - **Audit logging** — all operations are logged with caller identity, session ID, and timestamps
719
-
720
- ### Remote Mode Only
721
-
722
- These additional protections apply when running the two-component remote architecture:
723
-
724
- - **Zero secrets on the client** — the local MCP proxy never sees API keys or tokens
725
- - **Mutual authentication** — both sides prove their identity using Ed25519 signatures before any data is exchanged
726
- - **End-to-end encryption** — all requests/responses are encrypted with AES-256-GCM session keys derived via X25519 ECDH
727
- - **Replay protection** — monotonic counters prevent replay attacks
728
- - **Session isolation** — each handshake produces unique session keys with a 30-minute TTL
729
- - **File permissions** — private keys are saved with `0600`, directories with `0700`
730
-
731
- ### Local Mode Caveat
732
-
733
- 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.
734
-
735
517
  ## License
736
518
 
737
519
  MIT