lattice-talk 0.1.6 → 0.1.8
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 +15 -1
- package/dist/index.js +195 -7
- package/dist/index.js.map +1 -1
- package/dist/tui/main.js +274 -16
- package/dist/tui/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,6 +135,20 @@ Setup writes user-level settings to `~/.lattice/config.json` (restricted permiss
|
|
|
135
135
|
|
|
136
136
|
Without `LATTICE_REDIS_URL`, legacy Redis variables also work: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, `REDIS_DB`, `REDIS_SSL`.
|
|
137
137
|
|
|
138
|
+
**Custom / remote Redis** — point `LATTICE_REDIS_URL` (or the setup screen's Redis URL field) at any reachable Redis: local, Docker, a VM, or managed (Upstash, Redis Cloud, ElastiCache). `rediss://` enables TLS.
|
|
139
|
+
|
|
140
|
+
**Redis behind a bastion (SSH tunnel)** — set `LATTICE_SSH_HOST` and Lattice opens an `ssh -N -L` forward to your Redis before connecting. Works identically for the TUI, `serve`, `bridge`, and every harness-registered MCP server (the variables propagate).
|
|
141
|
+
|
|
142
|
+
| Variable | Default | Purpose |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `LATTICE_SSH_HOST` | — | Bastion host (`user@host` also works via `LATTICE_SSH_USER`) |
|
|
145
|
+
| `LATTICE_SSH_PORT` | `22` | SSH port on the bastion |
|
|
146
|
+
| `LATTICE_SSH_USER` | — | SSH user (else your ssh config/default user) |
|
|
147
|
+
| `LATTICE_SSH_KEY` | — | Identity file, e.g. `~/.ssh/id_ed25519` |
|
|
148
|
+
| `LATTICE_SSH_LOCAL_PORT` | auto | Pin the local forward port (default: free ephemeral port) |
|
|
149
|
+
|
|
150
|
+
Authentication must be non-interactive — ssh agent, key file, or `~/.ssh/config` (`BatchMode` is on, so a passphrase prompt never hangs the bus). `ssh` must be on PATH; on Windows 10+ it's the built-in OpenSSH client. TLS is dropped inside the tunnel — SSH already encrypts that hop, and `rediss://` through the forward would fail certificate checks against `127.0.0.1`.
|
|
151
|
+
|
|
138
152
|
## Security model
|
|
139
153
|
|
|
140
154
|
- Agent identity is owned by the joined MCP process — a model can't claim another agent's id on later calls.
|
|
@@ -150,7 +164,7 @@ Don't paste passwords, API keys, or secrets into messages or shared memory.
|
|
|
150
164
|
|
|
151
165
|
**Dashboard won't open** — install Bun, or Node.js 26.4+. `serve` still works on Node 20+.
|
|
152
166
|
|
|
153
|
-
**Dashboard opens but looks wrong** — garbled icons or washed-out colors on an old terminal mean the capability detection missed. Try `LATTICE_ASCII=1` for plain-ASCII glyphs, or set `COLORTERM=truecolor` if your terminal does support 24-bit color. `NO_COLOR=1` gives a clean monochrome UI.
|
|
167
|
+
**Dashboard opens but looks wrong** — garbled icons or washed-out colors on an old terminal mean the capability detection missed. Try `LATTICE_ASCII=1` for plain-ASCII glyphs, or set `COLORTERM=truecolor` if your terminal does support 24-bit color. `NO_COLOR=1` gives a clean monochrome UI, and `LATTICE_NO_ANIM=1` disables all animation (also automatic on legacy consoles). If it still renders badly, run with `LATTICE_TUI_LOG=/path/to/log.txt` once — the log records the detected terminal capabilities, palette tier, and any renderer errors, which pinpoints the cause. The dashboard header shows the running version so you can confirm you're not on a cached release.
|
|
154
168
|
|
|
155
169
|
**Agents can't see each other** — confirm every harness uses the same Redis, namespace, and workspace name, and the same `LATTICE_JOIN_TOKEN` if the workspace is protected. Restart the harness after changing MCP config.
|
|
156
170
|
|
package/dist/index.js
CHANGED
|
@@ -19021,7 +19021,7 @@ init_esm_shims();
|
|
|
19021
19021
|
|
|
19022
19022
|
// src/cli.ts
|
|
19023
19023
|
init_esm_shims();
|
|
19024
|
-
import { spawn as
|
|
19024
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
19025
19025
|
import { existsSync as existsSync3 } from "fs";
|
|
19026
19026
|
import { homedir as homedir3 } from "os";
|
|
19027
19027
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -19052,6 +19052,10 @@ function loadFileConfig(env = process.env, home = homedir()) {
|
|
|
19052
19052
|
if (typeof raw.namespace === "string" && raw.namespace.trim()) config2.namespace = raw.namespace.trim();
|
|
19053
19053
|
if (typeof raw.workspace === "string" && raw.workspace.trim()) config2.workspace = raw.workspace.trim();
|
|
19054
19054
|
if (typeof raw.joinToken === "string" && raw.joinToken.trim()) config2.joinToken = raw.joinToken.trim();
|
|
19055
|
+
if (typeof raw.sshHost === "string" && raw.sshHost.trim()) config2.sshHost = raw.sshHost.trim();
|
|
19056
|
+
if (typeof raw.sshPort === "string" && raw.sshPort.trim()) config2.sshPort = raw.sshPort.trim();
|
|
19057
|
+
if (typeof raw.sshUser === "string" && raw.sshUser.trim()) config2.sshUser = raw.sshUser.trim();
|
|
19058
|
+
if (typeof raw.sshKey === "string" && raw.sshKey.trim()) config2.sshKey = raw.sshKey.trim();
|
|
19055
19059
|
return { path: path2, config: config2, exists: true, corrupt: false };
|
|
19056
19060
|
} catch {
|
|
19057
19061
|
return { path: path2, config: {}, exists: true, corrupt: true };
|
|
@@ -19062,7 +19066,11 @@ function resolveConnection(file2, env = process.env) {
|
|
|
19062
19066
|
redisUrl: env.LATTICE_REDIS_URL?.trim() || env.REDIS_URL?.trim() || file2.redisUrl,
|
|
19063
19067
|
namespace: env.LATTICE_NAMESPACE?.trim() || file2.namespace || "dev",
|
|
19064
19068
|
workspace: env.LATTICE_DEFAULT_SESSION_ID?.trim() || file2.workspace,
|
|
19065
|
-
joinToken: env.LATTICE_JOIN_TOKEN?.trim() || file2.joinToken
|
|
19069
|
+
joinToken: env.LATTICE_JOIN_TOKEN?.trim() || file2.joinToken,
|
|
19070
|
+
sshHost: env.LATTICE_SSH_HOST?.trim() || file2.sshHost,
|
|
19071
|
+
sshPort: env.LATTICE_SSH_PORT?.trim() || file2.sshPort,
|
|
19072
|
+
sshUser: env.LATTICE_SSH_USER?.trim() || file2.sshUser,
|
|
19073
|
+
sshKey: env.LATTICE_SSH_KEY?.trim() || file2.sshKey
|
|
19066
19074
|
};
|
|
19067
19075
|
}
|
|
19068
19076
|
function connectionToEnv(conn) {
|
|
@@ -19071,6 +19079,10 @@ function connectionToEnv(conn) {
|
|
|
19071
19079
|
if (conn.namespace) env.LATTICE_NAMESPACE = conn.namespace;
|
|
19072
19080
|
if (conn.workspace) env.LATTICE_DEFAULT_SESSION_ID = conn.workspace;
|
|
19073
19081
|
if (conn.joinToken) env.LATTICE_JOIN_TOKEN = conn.joinToken;
|
|
19082
|
+
if (conn.sshHost) env.LATTICE_SSH_HOST = conn.sshHost;
|
|
19083
|
+
if (conn.sshPort) env.LATTICE_SSH_PORT = conn.sshPort;
|
|
19084
|
+
if (conn.sshUser) env.LATTICE_SSH_USER = conn.sshUser;
|
|
19085
|
+
if (conn.sshKey) env.LATTICE_SSH_KEY = conn.sshKey;
|
|
19074
19086
|
return env;
|
|
19075
19087
|
}
|
|
19076
19088
|
var HARNESS_ENV_KEYS = [
|
|
@@ -19080,6 +19092,10 @@ var HARNESS_ENV_KEYS = [
|
|
|
19080
19092
|
"REDIS_PASSWORD",
|
|
19081
19093
|
"REDIS_DB",
|
|
19082
19094
|
"REDIS_SSL",
|
|
19095
|
+
"LATTICE_SSH_HOST",
|
|
19096
|
+
"LATTICE_SSH_PORT",
|
|
19097
|
+
"LATTICE_SSH_USER",
|
|
19098
|
+
"LATTICE_SSH_KEY",
|
|
19083
19099
|
"LATTICE_PRESENCE_TTL",
|
|
19084
19100
|
"LATTICE_STREAM_MAXLEN",
|
|
19085
19101
|
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
@@ -19362,6 +19378,11 @@ function loadConfig(env = process.env) {
|
|
|
19362
19378
|
redisPassword: env.REDIS_PASSWORD || void 0,
|
|
19363
19379
|
redisDb: envIntInRange(env, "REDIS_DB", 0, 0, 9999),
|
|
19364
19380
|
redisSsl: envFlag(env.REDIS_SSL) || sslFromUrl,
|
|
19381
|
+
sshHost: env.LATTICE_SSH_HOST?.trim() || void 0,
|
|
19382
|
+
sshPort: envPort(env, "LATTICE_SSH_PORT", 22),
|
|
19383
|
+
sshUser: env.LATTICE_SSH_USER?.trim() || void 0,
|
|
19384
|
+
sshKey: env.LATTICE_SSH_KEY?.trim() || void 0,
|
|
19385
|
+
sshLocalPort: envPort(env, "LATTICE_SSH_LOCAL_PORT", 0) || void 0,
|
|
19365
19386
|
otelEndpoint: env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || void 0,
|
|
19366
19387
|
otelServiceName: (env.OTEL_SERVICE_NAME ?? "lattice-talk").trim() || "lattice-talk",
|
|
19367
19388
|
otelHeaders: parseOtelHeaders(env.OTEL_EXPORTER_OTLP_HEADERS),
|
|
@@ -21086,7 +21107,7 @@ init_esm_shims();
|
|
|
21086
21107
|
// package.json
|
|
21087
21108
|
var package_default = {
|
|
21088
21109
|
name: "lattice-talk",
|
|
21089
|
-
version: "0.1.
|
|
21110
|
+
version: "0.1.8",
|
|
21090
21111
|
description: "Lattice \u2014 Cross-Harness MCP Session Bus. Shared sessions, DMs, rooms, memory, and OpenTelemetry for agents across Claude Code, Cursor, Codex, and custom harnesses.",
|
|
21091
21112
|
type: "module",
|
|
21092
21113
|
main: "./dist/index.js",
|
|
@@ -22401,6 +22422,155 @@ var RedisStore = class _RedisStore {
|
|
|
22401
22422
|
}
|
|
22402
22423
|
};
|
|
22403
22424
|
|
|
22425
|
+
// src/core/ssh-tunnel.ts
|
|
22426
|
+
init_esm_shims();
|
|
22427
|
+
import { spawn } from "child_process";
|
|
22428
|
+
import { createConnection, createServer } from "net";
|
|
22429
|
+
function buildSshArgs(config2, localPort, targetHost, targetPort) {
|
|
22430
|
+
const args = [
|
|
22431
|
+
"-N",
|
|
22432
|
+
"-T",
|
|
22433
|
+
"-o",
|
|
22434
|
+
"BatchMode=yes",
|
|
22435
|
+
"-o",
|
|
22436
|
+
"ExitOnForwardFailure=yes",
|
|
22437
|
+
"-o",
|
|
22438
|
+
"StrictHostKeyChecking=accept-new",
|
|
22439
|
+
"-o",
|
|
22440
|
+
"ServerAliveInterval=15",
|
|
22441
|
+
"-o",
|
|
22442
|
+
"ServerAliveCountMax=2",
|
|
22443
|
+
"-L",
|
|
22444
|
+
`127.0.0.1:${localPort}:${targetHost}:${targetPort}`
|
|
22445
|
+
];
|
|
22446
|
+
if (config2.sshPort && config2.sshPort !== 22) args.push("-p", String(config2.sshPort));
|
|
22447
|
+
if (config2.sshKey) args.push("-i", config2.sshKey);
|
|
22448
|
+
const userHost = config2.sshUser ? `${config2.sshUser}@${config2.sshHost}` : config2.sshHost;
|
|
22449
|
+
args.push(userHost);
|
|
22450
|
+
return args;
|
|
22451
|
+
}
|
|
22452
|
+
async function freePort() {
|
|
22453
|
+
const server = createServer();
|
|
22454
|
+
await new Promise((resolve, reject) => {
|
|
22455
|
+
server.once("error", reject);
|
|
22456
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
22457
|
+
});
|
|
22458
|
+
const address = server.address();
|
|
22459
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
22460
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
22461
|
+
if (!port) throw new Error("could not allocate a local port for the SSH tunnel");
|
|
22462
|
+
return port;
|
|
22463
|
+
}
|
|
22464
|
+
function waitForPort(port, timeoutMs) {
|
|
22465
|
+
const deadline = Date.now() + timeoutMs;
|
|
22466
|
+
return new Promise((resolve, reject) => {
|
|
22467
|
+
const attempt = () => {
|
|
22468
|
+
const socket = createConnection({ host: "127.0.0.1", port });
|
|
22469
|
+
socket.once("connect", () => {
|
|
22470
|
+
socket.destroy();
|
|
22471
|
+
resolve();
|
|
22472
|
+
});
|
|
22473
|
+
socket.once("error", () => {
|
|
22474
|
+
socket.destroy();
|
|
22475
|
+
if (Date.now() >= deadline) {
|
|
22476
|
+
reject(new Error("SSH tunnel did not become ready in time"));
|
|
22477
|
+
} else {
|
|
22478
|
+
setTimeout(attempt, 120);
|
|
22479
|
+
}
|
|
22480
|
+
});
|
|
22481
|
+
};
|
|
22482
|
+
attempt();
|
|
22483
|
+
});
|
|
22484
|
+
}
|
|
22485
|
+
function tunneledConfig(config2, localPort) {
|
|
22486
|
+
let auth = "";
|
|
22487
|
+
let db = "";
|
|
22488
|
+
if (config2.redisUrl) {
|
|
22489
|
+
try {
|
|
22490
|
+
const url2 = new URL(config2.redisUrl);
|
|
22491
|
+
const user = decodeURIComponent(url2.username);
|
|
22492
|
+
const pass = decodeURIComponent(url2.password);
|
|
22493
|
+
if (user || pass) auth = `${encodeURIComponent(user)}:${encodeURIComponent(pass)}@`;
|
|
22494
|
+
if (url2.pathname && url2.pathname !== "/") db = url2.pathname;
|
|
22495
|
+
} catch {
|
|
22496
|
+
}
|
|
22497
|
+
} else {
|
|
22498
|
+
const user = config2.redisUsername;
|
|
22499
|
+
const pass = config2.redisPassword;
|
|
22500
|
+
if (user || pass) {
|
|
22501
|
+
auth = `${encodeURIComponent(user ?? "")}:${encodeURIComponent(pass ?? "")}@`;
|
|
22502
|
+
}
|
|
22503
|
+
if (config2.redisDb) db = `/${config2.redisDb}`;
|
|
22504
|
+
}
|
|
22505
|
+
return {
|
|
22506
|
+
...config2,
|
|
22507
|
+
redisUrl: `redis://${auth}127.0.0.1:${localPort}${db}`,
|
|
22508
|
+
redisSsl: false
|
|
22509
|
+
};
|
|
22510
|
+
}
|
|
22511
|
+
function tunnelTarget(config2) {
|
|
22512
|
+
if (config2.redisUrl) {
|
|
22513
|
+
try {
|
|
22514
|
+
const url2 = new URL(config2.redisUrl);
|
|
22515
|
+
return { host: url2.hostname, port: Number.parseInt(url2.port, 10) || 6379 };
|
|
22516
|
+
} catch {
|
|
22517
|
+
}
|
|
22518
|
+
}
|
|
22519
|
+
return { host: config2.redisHost ?? "127.0.0.1", port: config2.redisPort };
|
|
22520
|
+
}
|
|
22521
|
+
async function openSshTunnel(config2, options) {
|
|
22522
|
+
if (!config2.sshHost) return void 0;
|
|
22523
|
+
const target = tunnelTarget(config2);
|
|
22524
|
+
const localPort = config2.sshLocalPort || await freePort();
|
|
22525
|
+
const args = buildSshArgs(config2, localPort, target.host, target.port);
|
|
22526
|
+
let proc;
|
|
22527
|
+
try {
|
|
22528
|
+
proc = spawn("ssh", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
22529
|
+
} catch (e) {
|
|
22530
|
+
throw new Error(
|
|
22531
|
+
`SSH tunnel requested (LATTICE_SSH_HOST) but spawning ssh failed: ${e instanceof Error ? e.message : String(e)}`
|
|
22532
|
+
);
|
|
22533
|
+
}
|
|
22534
|
+
let stderrTail = "";
|
|
22535
|
+
proc.stderr?.on("data", (chunk) => {
|
|
22536
|
+
stderrTail = (stderrTail + String(chunk)).slice(-2e3);
|
|
22537
|
+
});
|
|
22538
|
+
const spawnError = new Promise((_, reject) => {
|
|
22539
|
+
proc.once(
|
|
22540
|
+
"error",
|
|
22541
|
+
() => reject(
|
|
22542
|
+
new Error(
|
|
22543
|
+
"SSH tunnel requested (LATTICE_SSH_HOST) but `ssh` is not on PATH. Install an OpenSSH client (built into Windows 10+ via Optional Features) or unset LATTICE_SSH_HOST."
|
|
22544
|
+
)
|
|
22545
|
+
)
|
|
22546
|
+
);
|
|
22547
|
+
proc.once(
|
|
22548
|
+
"exit",
|
|
22549
|
+
(code) => reject(
|
|
22550
|
+
new Error(
|
|
22551
|
+
`ssh exited before the tunnel was ready (code ${code}).` + (stderrTail.trim() ? ` ssh says: ${stderrTail.trim().split("\n").pop()}` : "") + " Check LATTICE_SSH_HOST/USER/KEY \u2014 authentication must work non-interactively (ssh-agent or key file)."
|
|
22552
|
+
)
|
|
22553
|
+
)
|
|
22554
|
+
);
|
|
22555
|
+
});
|
|
22556
|
+
try {
|
|
22557
|
+
await Promise.race([waitForPort(localPort, options?.timeoutMs ?? 1e4), spawnError]);
|
|
22558
|
+
} catch (e) {
|
|
22559
|
+
proc.kill();
|
|
22560
|
+
throw e;
|
|
22561
|
+
}
|
|
22562
|
+
return {
|
|
22563
|
+
localPort,
|
|
22564
|
+
target: `${target.host}:${target.port}`,
|
|
22565
|
+
close: () => new Promise((resolve) => {
|
|
22566
|
+
if (proc.exitCode !== null || proc.killed) return resolve();
|
|
22567
|
+
proc.once("exit", () => resolve());
|
|
22568
|
+
proc.kill();
|
|
22569
|
+
setTimeout(resolve, 2e3).unref();
|
|
22570
|
+
})
|
|
22571
|
+
};
|
|
22572
|
+
}
|
|
22573
|
+
|
|
22404
22574
|
// src/core/store.ts
|
|
22405
22575
|
async function createStore(config2, options) {
|
|
22406
22576
|
if (config2.store === "memory") {
|
|
@@ -22411,7 +22581,25 @@ async function createStore(config2, options) {
|
|
|
22411
22581
|
"LATTICE_STORE=redis requires LATTICE_REDIS_URL or REDIS_HOST. Use LATTICE_STORE=memory for single-process smoke tests."
|
|
22412
22582
|
);
|
|
22413
22583
|
}
|
|
22414
|
-
|
|
22584
|
+
const tunnel = await openSshTunnel(config2);
|
|
22585
|
+
if (!tunnel) {
|
|
22586
|
+
return RedisStore.connect(config2);
|
|
22587
|
+
}
|
|
22588
|
+
try {
|
|
22589
|
+
const store = await RedisStore.connect(tunneledConfig(config2, tunnel.localPort));
|
|
22590
|
+
const closeStore = store.close.bind(store);
|
|
22591
|
+
store.close = async () => {
|
|
22592
|
+
try {
|
|
22593
|
+
await closeStore();
|
|
22594
|
+
} finally {
|
|
22595
|
+
await tunnel.close();
|
|
22596
|
+
}
|
|
22597
|
+
};
|
|
22598
|
+
return store;
|
|
22599
|
+
} catch (e) {
|
|
22600
|
+
await tunnel.close();
|
|
22601
|
+
throw e;
|
|
22602
|
+
}
|
|
22415
22603
|
}
|
|
22416
22604
|
|
|
22417
22605
|
// src/bridge/acp.ts
|
|
@@ -22419,7 +22607,7 @@ init_esm_shims();
|
|
|
22419
22607
|
|
|
22420
22608
|
// src/bridge/ndjson-rpc.ts
|
|
22421
22609
|
init_esm_shims();
|
|
22422
|
-
import { spawn } from "child_process";
|
|
22610
|
+
import { spawn as spawn2 } from "child_process";
|
|
22423
22611
|
import { createInterface } from "readline";
|
|
22424
22612
|
var NdjsonRpc = class {
|
|
22425
22613
|
proc;
|
|
@@ -22429,7 +22617,7 @@ var NdjsonRpc = class {
|
|
|
22429
22617
|
notificationHandlers = /* @__PURE__ */ new Map();
|
|
22430
22618
|
closed = false;
|
|
22431
22619
|
constructor(command, args, opts = {}) {
|
|
22432
|
-
this.proc =
|
|
22620
|
+
this.proc = spawn2(command, args, {
|
|
22433
22621
|
cwd: opts.cwd,
|
|
22434
22622
|
env: { ...process.env, ...opts.env },
|
|
22435
22623
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -58143,7 +58331,7 @@ async function runTui(tuiArgs) {
|
|
|
58143
58331
|
);
|
|
58144
58332
|
return 1;
|
|
58145
58333
|
}
|
|
58146
|
-
const child =
|
|
58334
|
+
const child = spawn3(runtime.cmd, [...runtime.preArgs, tuiEntryPath(), ...tuiArgs], {
|
|
58147
58335
|
stdio: "inherit",
|
|
58148
58336
|
env: process.env
|
|
58149
58337
|
});
|