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

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
@@ -108,11 +108,11 @@ For custom setups (different aliases, multiple callers, different machines), you
108
108
  **1. Generate keys:**
109
109
 
110
110
  ```bash
111
- drawlatch generate-keys local my-laptop
112
- drawlatch generate-keys remote
111
+ drawlatch generate-keys caller my-laptop
112
+ drawlatch generate-keys server
113
113
  ```
114
114
 
115
- **2. Exchange public keys** — copy `*.pub.pem` files into the appropriate `keys/peers/` subdirectories. See [Key Exchange](#key-exchange) for details.
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.
116
116
 
117
117
  **3. Create configs** — copy the example files and edit:
118
118
 
@@ -165,7 +165,6 @@ Once connected, agents get these tools:
165
165
  {
166
166
  "host": "0.0.0.0",
167
167
  "port": 9999,
168
- "localKeysDir": "~/.drawlatch/keys/remote",
169
168
  "connectors": [],
170
169
  "callers": {},
171
170
  "rateLimitPerMinute": 60
@@ -176,11 +175,12 @@ Once connected, agents get these tools:
176
175
  |-------|-------------|---------|
177
176
  | `host` | Network interface to bind | `127.0.0.1` |
178
177
  | `port` | Listen port | `9999` |
179
- | `localKeysDir` | Path to server's own keypair | `~/.drawlatch/keys/remote` |
180
178
  | `connectors` | Custom connector definitions (see below) | `[]` |
181
179
  | `callers` | Per-caller access control (see below) | `{}` |
182
180
  | `rateLimitPerMinute` | Max requests per minute per session | `60` |
183
181
 
182
+ Server keys are always loaded from `keys/server/` inside the config directory.
183
+
184
184
  ### Callers
185
185
 
186
186
  Each caller is identified by their public key and declares which connections they can access:
@@ -190,7 +190,6 @@ Each caller is identified by their public key and declares which connections the
190
190
  "callers": {
191
191
  "alice": {
192
192
  "name": "Alice (senior engineer)",
193
- "peerKeyDir": "~/.drawlatch/keys/peers/alice",
194
193
  "connections": ["github", "stripe", "internal-api"],
195
194
  "env": {
196
195
  "GITHUB_TOKEN": "${ALICE_GITHUB_TOKEN}"
@@ -198,16 +197,16 @@ Each caller is identified by their public key and declares which connections the
198
197
  },
199
198
  "ci-server": {
200
199
  "name": "GitHub Actions CI",
201
- "peerKeyDir": "~/.drawlatch/keys/peers/ci-server",
202
200
  "connections": ["github"]
203
201
  }
204
202
  }
205
203
  }
206
204
  ```
207
205
 
206
+ Caller public keys are loaded automatically from `keys/callers/<alias>/` — no path configuration needed.
207
+
208
208
  | Field | Required | Description |
209
209
  |-------|----------|-------------|
210
- | `peerKeyDir` | Yes | Path to this caller's public key files |
211
210
  | `connections` | Yes | Array of connection names (built-in or custom connector aliases) |
212
211
  | `name` | No | Human-readable name for audit logs |
213
212
  | `env` | No | Per-caller env var overrides — redirect secret resolution per caller |
@@ -216,7 +215,9 @@ Each caller is identified by their public key and declares which connections the
216
215
  The `env` map lets multiple callers share the same connection with different credentials:
217
216
  - Keys are the env var names connectors reference (e.g., `GITHUB_TOKEN`)
218
217
  - Values are `"${REAL_ENV_VAR}"` (redirect) or literal strings (direct injection)
219
- - Checked before `process.env` during secret resolution
218
+ - Checked before prefixed env vars during secret resolution
219
+
220
+ Without an explicit `env` mapping, secrets resolve via prefixed env vars (e.g., caller "alice" + `GITHUB_TOKEN` → `ALICE_GITHUB_TOKEN`).
220
221
 
221
222
  ### Custom Connectors
222
223
 
@@ -256,8 +257,6 @@ Used by the local MCP proxy to connect to the remote server:
256
257
  ```json
257
258
  {
258
259
  "remoteUrl": "http://127.0.0.1:9999",
259
- "localKeyAlias": "my-laptop",
260
- "remotePublicKeysDir": "~/.drawlatch/keys/peers/remote-server",
261
260
  "connectTimeout": 10000,
262
261
  "requestTimeout": 30000
263
262
  }
@@ -266,13 +265,12 @@ Used by the local MCP proxy to connect to the remote server:
266
265
  | Field | Description | Default |
267
266
  |-------|-------------|---------|
268
267
  | `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
268
  | `connectTimeout` | Handshake timeout (ms) | `10000` |
273
269
  | `requestTimeout` | Request timeout (ms) | `30000` |
274
270
 
275
- Key alias resolution order: `MCP_KEY_ALIAS` env var > `localKeyAlias` > `localKeysDir` > `keys/local/default`.
271
+ Key paths are derived automatically no configuration needed:
272
+ - Caller keys: `keys/callers/{MCP_KEY_ALIAS || "default"}/`
273
+ - Server public keys: `keys/server/`
276
274
 
277
275
  ### Advanced Configuration
278
276
 
@@ -337,40 +335,27 @@ See **[INGESTORS.md](INGESTORS.md)** for full configuration reference.
337
335
 
338
336
  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.
339
337
 
340
- For multi-machine setups, exchange public keys manually:
341
-
342
338
  **Directory structure:**
343
339
 
344
340
  ```
345
341
  ~/.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)
342
+ ├── callers/
343
+ ├── default/ # Default caller keypair
344
+ └── alice/ # Additional caller keypair
345
+ └── server/ # Server keypair
351
346
  ```
352
347
 
353
- **Exchange public keys** (`.pub.pem` only never share private keys):
348
+ 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.
354
349
 
355
- ```bash
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
363
- ```
364
-
365
- If the proxy and server are on different machines, transfer only `*.pub.pem` files via `scp` or similar.
350
+ **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.
366
351
 
367
352
  ### Multiple Agent Identities
368
353
 
369
354
  Generate a keypair per agent and set `MCP_KEY_ALIAS` at spawn time:
370
355
 
371
356
  ```bash
372
- drawlatch generate-keys local alice
373
- drawlatch generate-keys local bob
357
+ drawlatch generate-keys caller alice
358
+ drawlatch generate-keys caller bob
374
359
  ```
375
360
 
376
361
  ```json
@@ -385,7 +370,7 @@ drawlatch generate-keys local bob
385
370
  }
386
371
  ```
387
372
 
388
- Register each agent as a separate caller on the remote server with matching peer key directories.
373
+ Register each agent as a separate caller in `remote.config.json`.
389
374
 
390
375
  ## CLI Reference
391
376
 
@@ -402,6 +387,7 @@ Commands:
402
387
  config Show effective configuration and secret status
403
388
  doctor Validate setup and diagnose issues
404
389
  generate-keys Generate Ed25519 + X25519 keypairs
390
+ sync Exchange keys with a callboard instance
405
391
 
406
392
  Options:
407
393
  -h, --help Show help
@@ -422,10 +408,13 @@ Logs options:
422
408
  --follow Tail the log output
423
409
 
424
410
  Generate-keys subcommands:
425
- local [alias] Generate local proxy keypair (default alias: "default")
426
- remote Generate remote server keypair
411
+ caller [alias] Generate caller keypair (default alias: "default")
412
+ server Generate server keypair
427
413
  show <path> Show fingerprint of existing keypair
428
414
  --dir <path> Generate to custom directory
415
+
416
+ Sync options:
417
+ --ttl <seconds> Session timeout (default: 300)
429
418
  ```
430
419
 
431
420
  ## Library Usage (Local Mode)
package/bin/drawlatch.js CHANGED
@@ -38,7 +38,7 @@ const { generateKeyBundle, saveKeyBundle, extractPublicKeys, fingerprint, loadKe
38
38
  );
39
39
 
40
40
  // Import connection template helpers
41
- const { listConnectionTemplates, listAvailableConnections } = await import(
41
+ const { listConnectionTemplates } = await import(
42
42
  join(PKG_ROOT, "dist/shared/connections.js")
43
43
  );
44
44
 
@@ -80,8 +80,6 @@ try {
80
80
  lines: { type: "string", short: "n", default: "50" },
81
81
  follow: { type: "boolean", default: false },
82
82
  path: { type: "boolean", default: false },
83
- connections: { type: "string" },
84
- alias: { type: "string" },
85
83
  full: { type: "boolean", default: false },
86
84
  ttl: { type: "string", default: "300" },
87
85
  },
@@ -105,7 +103,10 @@ if (values.version) {
105
103
  }
106
104
  if (values.help && !subcommand) {
107
105
  printHelp();
108
- const latestVersion = await updateCheckPromise;
106
+ const latestVersion = await Promise.race([
107
+ updateCheckPromise,
108
+ new Promise((r) => setTimeout(() => r(null), 100)),
109
+ ]);
109
110
  if (latestVersion) console.log(formatUpdateNotice(latestVersion));
110
111
  process.exit(0);
111
112
  }
@@ -187,7 +188,10 @@ switch (subcommand) {
187
188
  case "help":
188
189
  printHelp();
189
190
  {
190
- const latestVersion = await updateCheckPromise;
191
+ const latestVersion = await Promise.race([
192
+ updateCheckPromise,
193
+ new Promise((r) => setTimeout(() => r(null), 100)),
194
+ ]);
191
195
  if (latestVersion) console.log(formatUpdateNotice(latestVersion));
192
196
  }
193
197
  break;
@@ -207,28 +211,15 @@ async function cmdDefault() {
207
211
  console.log("Drawlatch remote server is not running.\n");
208
212
  printHelp();
209
213
  }
210
- const latestVersion = await updateCheckPromise;
214
+ // Show update notice only if the check already resolved (don't block on network)
215
+ const latestVersion = await Promise.race([
216
+ updateCheckPromise,
217
+ new Promise((r) => setTimeout(() => r(null), 100)),
218
+ ]);
211
219
  if (latestVersion) console.log(formatUpdateNotice(latestVersion));
212
220
  }
213
221
 
214
222
  async function cmdInit() {
215
- const alias = values.alias || "default";
216
- const connectionsList = values.connections
217
- ? values.connections.split(",").map((c) => c.trim()).filter(Boolean)
218
- : [];
219
-
220
- // Validate requested connections exist
221
- if (connectionsList.length > 0) {
222
- const available = listAvailableConnections();
223
- const availableSet = new Set(available);
224
- const invalid = connectionsList.filter((c) => !availableSet.has(c));
225
- if (invalid.length > 0) {
226
- console.error(`Unknown connection(s): ${invalid.join(", ")}`);
227
- console.error(`Available: ${available.join(", ")}`);
228
- process.exit(1);
229
- }
230
- }
231
-
232
223
  console.log(`\nDrawlatch Setup`);
233
224
  console.log(`===============\n`);
234
225
 
@@ -251,20 +242,7 @@ async function cmdInit() {
251
242
  steps.push(`Server keys: CREATED (${fp})`);
252
243
  }
253
244
 
254
- // Step 3: Generate caller keypair
255
- const callerKeysDir = join(getCallerKeysDir(), alias);
256
- if (existsSync(join(callerKeysDir, "signing.key.pem"))) {
257
- const existing = loadKeyBundle(callerKeysDir);
258
- const fp = fingerprint(extractPublicKeys(existing));
259
- steps.push(`Caller keys (${alias}): already exist (${fp})`);
260
- } else {
261
- const bundle = generateKeyBundle();
262
- saveKeyBundle(bundle, callerKeysDir);
263
- const fp = fingerprint(extractPublicKeys(bundle));
264
- steps.push(`Caller keys (${alias}): CREATED (${fp})`);
265
- }
266
-
267
- // Step 4: Scaffold proxy.config.json
245
+ // Step 3: Scaffold proxy.config.json
268
246
  const proxyConfigPath = getProxyConfigPath();
269
247
  if (existsSync(proxyConfigPath)) {
270
248
  steps.push(`Proxy config: already exists`);
@@ -278,7 +256,7 @@ async function cmdInit() {
278
256
  steps.push(`Proxy config: CREATED`);
279
257
  }
280
258
 
281
- // Step 5: Scaffold remote.config.json
259
+ // Step 4: Scaffold remote.config.json
282
260
  const remoteConfigPath = getRemoteConfigPath();
283
261
  if (existsSync(remoteConfigPath)) {
284
262
  steps.push(`Remote config: already exists`);
@@ -287,44 +265,22 @@ async function cmdInit() {
287
265
  host: "0.0.0.0",
288
266
  port: 9999,
289
267
  rateLimitPerMinute: 60,
290
- callers: {
291
- [alias]: {
292
- name: alias === "default" ? "Default Caller" : alias,
293
- connections: connectionsList,
294
- },
295
- },
268
+ callers: {},
296
269
  };
297
270
  writeFileSync(remoteConfigPath, JSON.stringify(remoteConfig, null, 2) + "\n", { mode: 0o600 });
298
- steps.push(`Remote config: CREATED (caller "${alias}" with ${connectionsList.length} connection(s))`);
271
+ steps.push(`Remote config: CREATED`);
299
272
  }
300
273
 
301
- // Step 7: Scaffold .env file
274
+ // Step 5: Scaffold .env file
302
275
  if (existsSync(ENV_FILE)) {
303
276
  steps.push(`.env file: already exists`);
304
277
  } else {
305
278
  const envLines = [
306
279
  "# Drawlatch environment secrets",
307
- "# Uncomment and set tokens for your enabled connections",
280
+ "# Set tokens for your enabled connections",
281
+ "# Secrets are prefixed per caller (e.g., DEFAULT_GITHUB_TOKEN)",
308
282
  "",
309
283
  ];
310
-
311
- // Get secret info for requested connections (or all if none specified)
312
- const templates = listConnectionTemplates();
313
- const relevantTemplates = connectionsList.length > 0
314
- ? templates.filter((t) => connectionsList.includes(t.alias))
315
- : templates.filter((t) => ["github", "slack", "discord-bot", "openai", "anthropic"].includes(t.alias));
316
-
317
- for (const t of relevantTemplates) {
318
- envLines.push(`# ${t.name}`);
319
- for (const s of t.requiredSecrets) {
320
- envLines.push(`# ${s}=`);
321
- }
322
- for (const s of t.optionalSecrets) {
323
- envLines.push(`# ${s}=`);
324
- }
325
- envLines.push("");
326
- }
327
-
328
284
  writeFileSync(ENV_FILE, envLines.join("\n") + "\n", { mode: 0o600 });
329
285
  steps.push(`.env file: CREATED`);
330
286
  }
@@ -335,25 +291,10 @@ async function cmdInit() {
335
291
  }
336
292
 
337
293
  console.log(`\nSetup complete! Next steps:\n`);
338
-
339
- if (connectionsList.length > 0) {
340
- const templates = listConnectionTemplates();
341
- const enabledTemplates = templates.filter((t) => connectionsList.includes(t.alias));
342
- const allSecrets = enabledTemplates.flatMap((t) => t.requiredSecrets);
343
- if (allSecrets.length > 0) {
344
- console.log(` 1. Set your API secrets in ${ENV_FILE}:`);
345
- for (const s of [...new Set(allSecrets)]) {
346
- console.log(` ${s}=your_token_here`);
347
- }
348
- console.log();
349
- }
350
- } else {
351
- console.log(` 1. Edit ${remoteConfigPath} to add connections (e.g., "github", "slack")`);
352
- console.log(` Then set the required secrets in ${ENV_FILE}\n`);
353
- }
354
-
355
- console.log(` 2. Start the remote server:`);
294
+ console.log(` 1. Start the remote server:`);
356
295
  console.log(` drawlatch start\n`);
296
+ console.log(` 2. Add callers via key sync:`);
297
+ console.log(` drawlatch sync\n`);
357
298
  console.log(` 3. Verify your setup:`);
358
299
  console.log(` drawlatch doctor\n`);
359
300
  }
@@ -869,14 +810,14 @@ async function cmdSync() {
869
810
  console.log(
870
811
  ` Keys saved to: ${join(CONFIG_DIR, "keys", "callers", status.callerAlias)}/`,
871
812
  );
872
- console.log(
873
- `\nAdd connections for this caller in remote.config.json:`,
874
- );
813
+ console.log(`\nThe caller can now connect (no server restart needed).`);
814
+ console.log(`\nTo grant API access, add connections in ${join(CONFIG_DIR, "remote.config.json")}:`);
875
815
  console.log(` "callers": {`);
876
816
  console.log(` "${status.callerAlias}": {`);
877
817
  console.log(` "connections": ["github", "slack", ...]`);
878
818
  console.log(` }`);
879
819
  console.log(` }`);
820
+ console.log(`\nThen set the required secrets in ${ENV_FILE}`);
880
821
  console.log();
881
822
  process.exit(0);
882
823
  }
@@ -1167,7 +1108,7 @@ drawlatch v${VERSION}
1167
1108
  Usage: drawlatch [command] [options]
1168
1109
 
1169
1110
  Commands:
1170
- init Set up drawlatch (keys, config, .env) in one step
1111
+ init Set up drawlatch server (keys, config, .env)
1171
1112
  start Start the remote server (background by default)
1172
1113
  stop Stop the background remote server
1173
1114
  restart Restart the background remote server
@@ -1185,9 +1126,7 @@ Options:
1185
1126
  Running 'drawlatch' with no arguments shows status (if running) or this help.
1186
1127
 
1187
1128
  Examples:
1188
- drawlatch init Set up everything with defaults
1189
- drawlatch init --connections github Set up with GitHub connection
1190
- drawlatch init --alias mybot Set up with custom caller alias
1129
+ drawlatch init Set up the remote server
1191
1130
  drawlatch start Start remote server in background
1192
1131
  drawlatch start -f Start remote server in foreground
1193
1132
  drawlatch start -f --tunnel Start with a public tunnel for webhooks
@@ -1304,23 +1243,18 @@ function printInitHelp() {
1304
1243
  console.log(`
1305
1244
  drawlatch init
1306
1245
 
1307
- Set up drawlatch for first-time use. Generates keys, creates config
1308
- files, exchanges public keys, and scaffolds a .env template.
1246
+ Set up the drawlatch remote server. Generates server keys, creates
1247
+ config files, and scaffolds a .env template.
1248
+
1249
+ Callers are added separately via 'drawlatch sync' after the server
1250
+ is running.
1309
1251
 
1310
1252
  Usage: drawlatch init [options]
1311
1253
 
1312
1254
  Options:
1313
- --connections <list> Comma-separated connections to enable (e.g., github,slack)
1314
- --alias <name> Name for the local identity (default: "default")
1315
- -h, --help Show this help message
1255
+ -h, --help Show this help message
1316
1256
 
1317
1257
  All steps are idempotent — safe to re-run without overwriting existing files.
1318
-
1319
- Examples:
1320
- drawlatch init Set up with defaults
1321
- drawlatch init --connections github Set up with GitHub enabled
1322
- drawlatch init --alias laptop Use "laptop" as the caller alias
1323
- drawlatch init --alias ci --connections github,slack
1324
1258
  `);
1325
1259
  }
1326
1260
 
@@ -1416,14 +1416,22 @@ export function main() {
1416
1416
  }
1417
1417
  const config = loadRemoteConfig();
1418
1418
  const serverKeysDirPath = getServerKeysDir();
1419
- if (!fs.existsSync(serverKeysDirPath)) {
1420
- console.error(`[remote] Error: Server keys not found at ${serverKeysDirPath}`);
1419
+ const requiredKeyFiles = ['signing.key.pem', 'signing.pub.pem', 'exchange.key.pem', 'exchange.pub.pem'];
1420
+ const missingKeyFiles = requiredKeyFiles.filter((f) => !fs.existsSync(path.join(serverKeysDirPath, f)));
1421
+ if (missingKeyFiles.length > 0) {
1422
+ if (!fs.existsSync(serverKeysDirPath)) {
1423
+ console.error(`[remote] Error: Server keys not found at ${serverKeysDirPath}`);
1424
+ }
1425
+ else {
1426
+ console.error(`[remote] Error: Incomplete server keys in ${serverKeysDirPath}`);
1427
+ console.error(`[remote] Missing: ${missingKeyFiles.join(', ')}`);
1428
+ }
1421
1429
  console.error('[remote] Run: drawlatch generate-keys server');
1422
1430
  process.exit(1);
1423
1431
  }
1424
1432
  if (Object.keys(config.callers).length === 0) {
1425
- console.error('[remote] Warning: No callers configured. No clients will be able to connect.');
1426
- console.error('[remote] Add callers to remote.config.json or run: drawlatch init');
1433
+ console.log('[remote] No callers configured server will accept sync requests.');
1434
+ console.log('[remote] To add callers, run: drawlatch sync');
1427
1435
  }
1428
1436
  const port = process.env.DRAWLATCH_PORT ? parseInt(process.env.DRAWLATCH_PORT, 10) : config.port;
1429
1437
  const host = process.env.DRAWLATCH_HOST ?? config.host;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolpertingerlabs/drawlatch",
3
- "version": "1.0.0-alpha.9.0",
3
+ "version": "1.0.0-alpha.9.2",
4
4
  "description": "Encrypted MCP proxy with mutual authentication. Local MCP server forwards requests through an encrypted channel to a remote secrets-holding server.",
5
5
  "type": "module",
6
6
  "main": "./dist/mcp/server.js",