@wolpertingerlabs/drawlatch 1.0.0-alpha.3 → 1.0.0-alpha.35

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.
Files changed (123) hide show
  1. package/CONNECTIONS.md +3 -0
  2. package/README.md +393 -465
  3. package/bin/drawlatch.js +1151 -49
  4. package/dist/auth/auth.d.ts +10 -0
  5. package/dist/auth/auth.js +146 -0
  6. package/dist/auth/env-writer.d.ts +6 -0
  7. package/dist/auth/env-writer.js +52 -0
  8. package/dist/auth/password.d.ts +8 -0
  9. package/dist/auth/password.js +28 -0
  10. package/dist/auth/sessions.d.ts +13 -0
  11. package/dist/auth/sessions.js +85 -0
  12. package/dist/cli/generate-keys.d.ts +4 -4
  13. package/dist/cli/generate-keys.js +30 -25
  14. package/dist/connections/{anthropic.json → ai/anthropic.json} +19 -1
  15. package/dist/connections/{devin.json → ai/devin.json} +7 -1
  16. package/dist/connections/{google-ai.json → ai/google-ai.json} +7 -1
  17. package/dist/connections/{openai.json → ai/openai.json} +7 -1
  18. package/dist/connections/{openrouter.json → ai/openrouter.json} +7 -1
  19. package/dist/connections/developer-tools/github.json +138 -0
  20. package/dist/connections/{hex.json → developer-tools/hex.json} +7 -1
  21. package/dist/connections/developer-tools/linear.json +75 -0
  22. package/dist/connections/{lichess.json → gaming/lichess.json} +7 -1
  23. package/dist/connections/messaging/agentmail.json +21 -0
  24. package/dist/connections/messaging/discord-bot.json +114 -0
  25. package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +7 -1
  26. package/dist/connections/messaging/slack.json +75 -0
  27. package/dist/connections/messaging/telegram.json +66 -0
  28. package/dist/connections/{google.json → productivity/google.json} +7 -1
  29. package/dist/connections/productivity/notion.json +75 -0
  30. package/dist/connections/productivity/stripe.json +74 -0
  31. package/dist/connections/productivity/trello.json +113 -0
  32. package/dist/connections/{bluesky.json → social-media/bluesky.json} +41 -0
  33. package/dist/connections/social-media/mastodon.json +65 -0
  34. package/dist/connections/social-media/reddit.json +80 -0
  35. package/dist/connections/{twitch.json → social-media/twitch.json} +40 -0
  36. package/dist/connections/social-media/x.json +67 -0
  37. package/dist/mcp/server.js +544 -31
  38. package/dist/remote/admin-mutations.d.ts +43 -0
  39. package/dist/remote/admin-mutations.js +321 -0
  40. package/dist/remote/admin-types.d.ts +153 -0
  41. package/dist/remote/admin-types.js +11 -0
  42. package/dist/remote/admin.d.ts +37 -0
  43. package/dist/remote/admin.js +317 -0
  44. package/dist/remote/caller-bootstrap.d.ts +121 -0
  45. package/dist/remote/caller-bootstrap.js +270 -0
  46. package/dist/remote/ingestors/base-ingestor.d.ts +19 -3
  47. package/dist/remote/ingestors/base-ingestor.js +27 -6
  48. package/dist/remote/ingestors/discord/discord-gateway.d.ts +2 -2
  49. package/dist/remote/ingestors/discord/discord-gateway.js +29 -6
  50. package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
  51. package/dist/remote/ingestors/e2e/setup.js +147 -0
  52. package/dist/remote/ingestors/manager.d.ts +110 -10
  53. package/dist/remote/ingestors/manager.js +449 -42
  54. package/dist/remote/ingestors/poll/poll-ingestor.d.ts +4 -2
  55. package/dist/remote/ingestors/poll/poll-ingestor.js +26 -5
  56. package/dist/remote/ingestors/registry.d.ts +2 -2
  57. package/dist/remote/ingestors/registry.js +2 -2
  58. package/dist/remote/ingestors/slack/socket-mode.d.ts +2 -2
  59. package/dist/remote/ingestors/slack/socket-mode.js +28 -6
  60. package/dist/remote/ingestors/types.d.ts +25 -0
  61. package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +46 -7
  62. package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +115 -10
  63. package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +30 -0
  64. package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +73 -2
  65. package/dist/remote/ingestors/webhook/lifecycle-types.d.ts +63 -0
  66. package/dist/remote/ingestors/webhook/lifecycle-types.js +12 -0
  67. package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +2 -2
  68. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +17 -5
  69. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +32 -26
  70. package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.d.ts +46 -0
  71. package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +261 -0
  72. package/dist/remote/server.d.ts +25 -33
  73. package/dist/remote/server.js +596 -187
  74. package/dist/remote/tool-dispatch.d.ts +84 -0
  75. package/dist/remote/tool-dispatch.js +910 -0
  76. package/dist/remote/triggers/rule-engine.d.ts +40 -0
  77. package/dist/remote/triggers/rule-engine.js +228 -0
  78. package/dist/remote/triggers/types.d.ts +69 -0
  79. package/dist/remote/triggers/types.js +10 -0
  80. package/dist/remote/tunnel-state.d.ts +14 -0
  81. package/dist/remote/tunnel-state.js +20 -0
  82. package/dist/remote/tunnel.d.ts +13 -0
  83. package/dist/remote/tunnel.js +33 -0
  84. package/dist/shared/config.d.ts +104 -25
  85. package/dist/shared/config.js +49 -36
  86. package/dist/shared/connections.d.ts +21 -5
  87. package/dist/shared/connections.js +56 -14
  88. package/dist/shared/crypto/index.d.ts +1 -0
  89. package/dist/shared/crypto/index.js +1 -0
  90. package/dist/shared/crypto/key-manager.d.ts +81 -0
  91. package/dist/shared/crypto/key-manager.js +174 -0
  92. package/dist/shared/env-utils.d.ts +42 -0
  93. package/dist/shared/env-utils.js +150 -0
  94. package/dist/shared/listener-config.d.ts +157 -0
  95. package/dist/shared/listener-config.js +10 -0
  96. package/dist/shared/migrations.d.ts +40 -0
  97. package/dist/shared/migrations.js +122 -0
  98. package/dist/shared/protocol/caller-bundle-crypto.d.ts +37 -0
  99. package/dist/shared/protocol/caller-bundle-crypto.js +62 -0
  100. package/dist/shared/protocol/caller-bundle.d.ts +75 -0
  101. package/dist/shared/protocol/caller-bundle.js +24 -0
  102. package/dist/shared/protocol/handshake.js +14 -1
  103. package/dist/shared/protocol/index.d.ts +2 -0
  104. package/dist/shared/protocol/index.js +2 -0
  105. package/dist/shared/protocol/sync-client.d.ts +52 -0
  106. package/dist/shared/protocol/sync-client.js +99 -0
  107. package/dist/shared/protocol/sync.d.ts +71 -0
  108. package/dist/shared/protocol/sync.js +176 -0
  109. package/frontend/dist/assets/index-BdCSPSZK.js +282 -0
  110. package/frontend/dist/assets/index-BmK26bY2.css +1 -0
  111. package/frontend/dist/index.html +15 -0
  112. package/package.json +65 -13
  113. package/dist/connections/discord-bot.json +0 -24
  114. package/dist/connections/github.json +0 -25
  115. package/dist/connections/linear.json +0 -29
  116. package/dist/connections/mastodon.json +0 -25
  117. package/dist/connections/notion.json +0 -33
  118. package/dist/connections/reddit.json +0 -28
  119. package/dist/connections/slack.json +0 -23
  120. package/dist/connections/stripe.json +0 -25
  121. package/dist/connections/telegram.json +0 -26
  122. package/dist/connections/trello.json +0 -25
  123. package/dist/connections/x.json +0 -27
package/README.md CHANGED
@@ -1,686 +1,614 @@
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 23 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
+ - **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
- ### 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)
53
+ Get from zero to working in three commands:
56
54
 
57
- This repo is structured as a **Claude Code plugin** with a marketplace. Install it directly:
55
+ ```bash
56
+ # Install globally
57
+ npm install -g @wolpertingerlabs/drawlatch
58
58
 
59
- ```shell
60
- # Add the marketplace (from a local clone)
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
- # Install the plugin
64
- /plugin install drawlatch@drawlatch
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
- Or load it directly during development:
69
+ Verify your setup:
68
70
 
69
- ```shell
70
- claude --plugin-dir ./path/to/drawlatch
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
- Before using, set the `MCP_CONFIG_DIR` environment variable so the proxy can find its config and keys:
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
- ```bash
76
- export MCP_CONFIG_DIR=~/.drawlatch
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 when enabled. The `secure_request` and `list_routes` tools become available immediately.
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
- ### Option 2: Auto-Discovery (opening this repo directly)
90
+ **Option 2: Auto-Discovery**
82
91
 
83
- 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.
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
- Before approving, set the `MCP_CONFIG_DIR` environment variable:
94
+ **Option 3: Manual Registration**
86
95
 
87
96
  ```bash
88
- export MCP_CONFIG_DIR=~/.drawlatch
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
- The `.mcp.json` passes this through to the MCP server process. You also need a working setup (keys generated, public keys exchanged, configs in place, remote server running). See [Setup](#setup) below for the full walkthrough.
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
- > **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`.
104
+ ### Manual Setup
94
105
 
95
- ## Setup
106
+ For custom setups (different aliases, multiple callers, different machines), you can configure everything manually instead of using `drawlatch init`.
96
107
 
97
- ### Prerequisites
108
+ **1. Generate keys:**
98
109
 
99
110
  ```bash
100
- git clone <repo-url>
101
- cd drawlatch
102
- npm install
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
- ```bash
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
- # Or use the default alias
147
- npm run generate-keys -- local
117
+ **3. Create configs** — copy the example files and edit:
148
118
 
149
- # Generate remote server keypair
150
- npm run generate-keys -- remote
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
- 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.
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
- npm run generate-keys -- --dir /path/to/custom/keys
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
- Or inspect the fingerprint of an existing keypair:
133
+ **5. Start the server:**
164
134
 
165
135
  ```bash
166
- npm run generate-keys -- show ~/.drawlatch/keys/local/my-laptop
136
+ drawlatch start
137
+ drawlatch doctor # Validate full setup
167
138
  ```
168
139
 
169
- ### Step 2: Exchange Public Keys
140
+ ## Admin Dashboard
170
141
 
171
- 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):
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
- **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:
144
+ ### Architecture
174
145
 
175
- ```bash
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
- cp ~/.drawlatch/keys/local/my-laptop/signing.pub.pem \
179
- ~/.drawlatch/keys/peers/my-laptop/signing.pub.pem
180
-
181
- cp ~/.drawlatch/keys/local/my-laptop/exchange.pub.pem \
182
- ~/.drawlatch/keys/peers/my-laptop/exchange.pub.pem
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
- **From remote to local** copy the remote server's public keys into the proxy's peer directory:
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
- ```bash
188
- mkdir -p ~/.drawlatch/keys/peers/remote-server
161
+ ### Setup
189
162
 
190
- cp ~/.drawlatch/keys/remote/signing.pub.pem \
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
- cp ~/.drawlatch/keys/remote/exchange.pub.pem \
194
- ~/.drawlatch/keys/peers/remote-server/exchange.pub.pem
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
- > **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.
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
- ### Step 3: Create the Local Proxy Config
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
- Copy the example and edit the paths to match your setup:
174
+ ### Pages
202
175
 
203
- ```bash
204
- cp proxy.config.example.json ~/.drawlatch/proxy.config.json
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
- Edit `~/.drawlatch/proxy.config.json`:
186
+ ### The `/api/admin/*` API
208
187
 
209
- ```json
210
- {
211
- "remoteUrl": "http://127.0.0.1:9999",
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
- | Field | Description | Default |
220
- | --------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------- |
221
- | `remoteUrl` | URL of the remote secure server | `http://localhost:9999` |
222
- | `localKeyAlias` | Key alias resolved to `keys/local/<alias>/`. Overridden by `MCP_KEY_ALIAS` env var at runtime | _(none)_ |
223
- | `localKeysDir` | Absolute path to the proxy's own keypair directory. Ignored when `localKeyAlias` is set | `~/.drawlatch/keys/local/default` |
224
- | `remotePublicKeysDir` | Absolute path to the remote server's public keys | `~/.drawlatch/keys/peers/remote-server` |
225
- | `connectTimeout` | Handshake timeout in milliseconds | `10000` (10s) |
226
- | `requestTimeout` | Request timeout in milliseconds | `30000` (30s) |
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
- **Alias resolution priority:**
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
- 1. `MCP_KEY_ALIAS` env var (highest set per agent at spawn time)
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
- ### Step 4: Create the Remote Server Config
206
+ ### Security model
236
207
 
237
- Copy the example and edit it to match your setup:
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
- cp remote.config.example.json ~/.drawlatch/remote.config.json
211
+ DRAWLATCH_HOST=0.0.0.0 drawlatch start # or: drawlatch start --host 0.0.0.0
241
212
  ```
242
213
 
243
- Edit `~/.drawlatch/remote.config.json`. This is where you define your callers, their connections, custom connectors, and secrets.
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
- The config is **caller-centric** each caller is identified by their public key and explicitly declares which connections they can access.
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
- #### Example: Single caller with a built-in connection
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
- "localKeysDir": "~/.drawlatch/keys/remote",
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
- 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.
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
- #### Example: Multiple callers with different access levels
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
- "peerKeyDir": "/keys/peers/alice",
287
- "connections": ["github", "stripe", "internal-api"]
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
- 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.
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
- #### Example: Per-caller env overrides (shared connector, different credentials)
300
+ Without an explicit `env` mapping, secrets resolve via prefixed env vars (e.g., caller "alice" + `GITHUB_TOKEN` → `ALICE_GITHUB_TOKEN`).
302
301
 
303
- When multiple callers use the same connection but need different credentials, use the `env` field to redirect environment variable resolution per caller:
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
- "host": "0.0.0.0",
308
- "port": 9999,
309
- "localKeysDir": "/keys/server",
310
- "callers": {
311
- "alice": {
312
- "name": "Alice",
313
- "peerKeyDir": "/keys/peers/alice",
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
- The `env` map works as follows:
334
-
335
- - **Keys** are the env var names that connectors reference (e.g., `GITHUB_TOKEN`)
336
- - **Values** are either `"${REAL_ENV_VAR}"` (redirect to a different env var) or a literal string (direct injection)
337
- - When resolving secrets, the caller's `env` is checked **before** `process.env`
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
- 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.
331
+ Custom connectors with an `alias` matching a built-in connection name take precedence.
340
332
 
341
- #### Remote Config Reference
333
+ ### Proxy Config (`proxy.config.json`)
342
334
 
343
- | Field | Description | Default |
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
- #### Connector Definition
353
-
354
- Custom connectors define reusable route templates referenced by `alias` from caller connection lists. They follow the same structure as routes:
337
+ ```json
338
+ {
339
+ "remoteUrl": "http://127.0.0.1:9999",
340
+ "connectTimeout": 10000,
341
+ "requestTimeout": 30000
342
+ }
343
+ ```
355
344
 
356
- | Field | Required | Description |
357
- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
358
- | `alias` | Yes | Unique name for referencing this connector from caller `connections` lists |
359
- | `allowedEndpoints` | Yes | Array of glob patterns for allowed URLs (e.g., `https://api.example.com/**`) |
360
- | `name` | No | Human-readable name (e.g., `"Internal Admin API"`) |
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
- #### Caller Definition
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
- | Field | Required | Description |
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
- #### How Secrets Work
357
+ #### `MCP_CONFIG_DIR` the config-dir contract
379
358
 
380
- Secret values in the `secrets` map are resolved at session establishment time (per-caller):
359
+ By default, all config and key files live in `~/.drawlatch/`. Override with:
381
360
 
382
- - **Literal values** — used as-is: `"API_TOKEN": "sk_live_abc123"`
383
- - **Environment variable references** — resolved from the server's environment: `"API_TOKEN": "${API_TOKEN}"`
384
- - **Per-caller overrides** — when a caller has an `env` entry for a variable name, that value is used instead of `process.env`
361
+ ```bash
362
+ export MCP_CONFIG_DIR=/custom/path/to/config
363
+ ```
385
364
 
386
- Header values can reference secrets using `${VAR}` placeholders:
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
- ```json
389
- "headers": {
390
- "Authorization": "Bearer ${API_TOKEN}"
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
- 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.
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
- ### Connections (Pre-built Route Templates)
380
+ #### Self-managed tunnel
397
381
 
398
- 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:
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
- ```json
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
- See **[CONNECTIONS.md](CONNECTIONS.md)** for the full list of available connections, required environment variables, and usage examples.
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
- ### Step 5: Start the Servers
388
+ ## Connections
416
389
 
417
- **Start the remote server:**
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
- # Production (requires `npm run build` first)
424
- npm run start:remote
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
- **Connect the local MCP proxy to Claude Code:**
417
+ See **[CONNECTIONS.md](CONNECTIONS.md)** for auth details, optional env vars, and usage notes per connection.
428
418
 
429
- 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.
419
+ ## Event Ingestion
430
420
 
431
- The `.mcp.json` requires the `MCP_CONFIG_DIR` environment variable to be set so the proxy can locate its config and keys. Set it to the absolute path of your `~/.drawlatch/` directory:
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
- ```bash
434
- export MCP_CONFIG_DIR=~/.drawlatch
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
- **Alternative: manual registration**
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
- If you prefer not to use auto-discovery, register the MCP server directly:
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
- ```bash
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
- After connecting (either via auto-discovery or manual registration), the proxy will automatically perform the encrypted handshake with the remote server on first use.
435
+ ## Key Exchange
449
436
 
450
- ### Step 6: Webhook Endpoints (Optional)
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
- 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`.
439
+ **Directory structure:**
453
440
 
454
- **Setup:**
441
+ ```
442
+ ~/.drawlatch/keys/
443
+ ├── callers/
444
+ │ ├── default/ # Default caller keypair
445
+ │ └── alice/ # Additional caller keypair
446
+ └── server/ # Server keypair
447
+ ```
455
448
 
456
- 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/))
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
- 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.
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 Agents (Multi-Identity)
453
+ ### Multiple Agent Identities
463
454
 
464
- When multiple agents share the same machine, each needs its own key identity. Generate a keypair per agent:
455
+ Generate a keypair per agent and set `MCP_KEY_ALIAS` at spawn time:
465
456
 
466
457
  ```bash
467
- npm run generate-keys -- local alice
468
- npm run generate-keys -- local bob
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
- "secure-proxy": {
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
- 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/`.
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
- Make an authenticated HTTP request through the proxy. Route-level headers (e.g., `Authorization`) are injected automatically — the agent never sees the secret values.
476
+ ## CLI Reference
497
477
 
498
478
  ```
499
- method: GET | POST | PUT | PATCH | DELETE
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
- Get the status of all active ingestors for the current caller. Returns connection state, buffer sizes, event counts, and any errors. Takes no parameters.
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
- ## Library Usage (Local Mode)
494
+ Options:
495
+ -h, --help Show help
496
+ -v, --version Show version
525
497
 
526
- 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:
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
- ```typescript
529
- // Core request execution (same function the remote server uses)
530
- import { executeProxyRequest } from "drawlatch/remote/server";
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
- // Config loading and route resolution
533
- import {
534
- loadRemoteConfig,
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
- // Ingestor management (WebSocket, webhook, poll)
541
- import { IngestorManager } from "drawlatch/remote/ingestors";
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
- // Crypto primitives (if building custom transport)
544
- import { loadKeyBundle, loadPublicKeys, EncryptedChannel } from "drawlatch/shared/crypto";
518
+ Sync options:
519
+ --ttl <seconds> Session timeout (default: 300)
545
520
  ```
546
521
 
547
- ### Available Exports
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
- ### Example: In-Process Proxy
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
- > **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.
579
-
580
- ## Development
541
+ ### Available Exports
581
542
 
582
- ```bash
583
- # Run tests
584
- npm test
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
- # Run tests in watch mode
587
- npm run test:watch
554
+ ## Security Model
588
555
 
589
- # Run tests with coverage
590
- npm run test:coverage
556
+ ### Both Modes
591
557
 
592
- # Lint
593
- npm run lint
594
- npm run lint:fix
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
- # Format
597
- npm run format
598
- npm run format:check
599
- ```
564
+ ### Remote Mode Only
600
565
 
601
- ## Architecture
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
- ### Plugin Structure
573
+ ## Development
604
574
 
605
- This repo is structured as a Claude Code plugin:
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
- drawlatch/
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 Code
586
+ ### Source Structure
619
587
 
620
588
  ```
621
589
  src/
622
- ├── cli/ # Key generation CLI
623
- │ └── generate-keys.ts # Ed25519 + X25519 keypair generation
624
- ├── connections/ # Pre-built route templates (JSON)
625
- ├── github.json # GitHub REST API
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 # Remote secure server (Express HTTP)
632
- │ ├── server.test.ts # Unit tests
633
- ├── server.e2e.test.ts # End-to-end tests
634
- └── ingestors/ # Real-time event ingestion system
635
- │ ├── base-ingestor.ts # Abstract base class (state machine, ring buffer)
636
- │ ├── ring-buffer.ts # Generic bounded circular buffer
637
- ├── manager.ts # Lifecycle management, per-caller routing
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 # Config loading/saving, caller & route resolution
646
- ├── connections.ts # Connection template loading
647
- ├── logger.ts # Structured logging
648
- ├── crypto/
649
- │ ├── keys.ts # Ed25519 + X25519 key generation/serialization
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
- ### Both Modes
661
-
662
- These protections apply regardless of whether you use remote or local mode:
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