feat(cli): 1.29.0 — per-session IPC tokens + auto-scoping

every claudemesh launch-spawned session now mints a 32-byte random
token, writes it under tmpdir (mode 0600), and registers it with the
daemon. cli invocations from inside that session inherit
CLAUDEMESH_IPC_TOKEN_FILE in env, attach the token via Authorization:
ClaudeMesh-Session <hex>, and the daemon resolves it to a SessionInfo.

server-side: every read route that filters by mesh now uses meshFromCtx —
explicit query/body wins, session default fills in when missing. write
routes follow the same pattern.

cli-side: peers.ts (and other multi-mesh-iterating verbs in future)
prefers session-token mesh over all joined meshes when the user didn't
pass --mesh explicitly.

backward-compatible in both directions — tokenless callers behave
exactly as before. registry is in-memory; daemon restart loses it but
the 30s reaper handles dead pids and most callers re-register on next
launch.

verified end-to-end: peer list with token returns 4 prueba1 peers,
without token returns 3 meshes' peers (aggregate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-04 12:33:06 +01:00
parent 81f0e4f7ac
commit 92cac16c91
10 changed files with 431 additions and 12 deletions

View File

@@ -0,0 +1,56 @@
/**
* CLI-side session resolver. Reads the session token from env, asks
* the daemon `GET /v1/sessions/me`, and caches the result for the
* lifetime of this CLI invocation.
*
* Used by verbs that iterate multiple meshes client-side (peer list,
* me, member list) so that, when invoked from inside a launched
* session, they auto-scope to that session's workspace instead of
* aggregating across every joined mesh.
*
* Returns null when:
* - no token in env (caller is outside a launched session, or
* bare `claudemesh` with no installed daemon).
* - token present but daemon doesn't recognize it (registry was
* reset by a daemon restart).
* - any IPC error (treat as "no scoping info, fall back to default
* behavior").
*/
import { ipc } from "~/daemon/ipc/client.js";
import { readSessionTokenFromEnv } from "./token.js";
export interface ResolvedSession {
sessionId: string;
mesh: string;
displayName: string;
pid: number;
cwd?: string;
role?: string;
groups?: string[];
}
let cached: ResolvedSession | null | undefined = undefined;
export async function getSessionInfo(): Promise<ResolvedSession | null> {
if (cached !== undefined) return cached;
const tok = readSessionTokenFromEnv();
if (!tok) { cached = null; return null; }
try {
const res = await ipc<{ session?: ResolvedSession }>({
path: "/v1/sessions/me",
timeoutMs: 1_500,
});
if (res.status !== 200 || !res.body.session) { cached = null; return null; }
cached = res.body.session;
return cached;
} catch {
cached = null;
return null;
}
}
/** Test helper. */
export function _resetSessionCache(): void {
cached = undefined;
}

View File

@@ -0,0 +1,53 @@
/**
* Per-session IPC tokens — mint, persist, read.
*
* Each `claudemesh launch` mints a 32-byte random token, writes it to
* `<tmpdir>/session-token` (mode 0o600), and exposes the path to the
* spawned `claude` via `CLAUDEMESH_IPC_TOKEN_FILE`. Subprocesses
* inheriting this env auto-attach the token to every IPC request via
* the `Authorization: ClaudeMesh-Session <hex>` header. The daemon's
* registry resolves the token to `{sessionId, mesh, displayName, pid,
* cwd, ...}` in O(1) and uses it for auto-scoping + attribution.
*
* Why a file path env var, not the value directly:
* `ps eww -p <pid>` shows env values to other processes of the same
* uid. The path leaks; the secret in mode-0600 files inside a
* mode-0700 tmpdir does not. Same trick OpenSSH uses for SSH_AUTH_SOCK.
*/
import { randomBytes } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
const ENV_TOKEN_FILE = "CLAUDEMESH_IPC_TOKEN_FILE";
export interface MintedToken {
token: string;
/** Filesystem path the token was written to. Pass via env to children. */
filePath: string;
}
/** Generate a fresh 64-hex token and write it under `dir`. */
export function mintSessionToken(dir: string, fileName = "session-token"): MintedToken {
const token = randomBytes(32).toString("hex");
const filePath = `${dir}/${fileName}`;
writeFileSync(filePath, token, { mode: 0o600 });
return { token, filePath };
}
/** Read a token from the path in CLAUDEMESH_IPC_TOKEN_FILE, if present.
* Falls back to a literal CLAUDEMESH_IPC_TOKEN env value (for testing).
* Returns null when neither is set or the file is unreadable. */
export function readSessionTokenFromEnv(env: NodeJS.ProcessEnv = process.env): string | null {
const direct = env.CLAUDEMESH_IPC_TOKEN;
if (direct && /^[0-9a-f]{64}$/i.test(direct)) return direct.toLowerCase();
const path = env[ENV_TOKEN_FILE];
if (!path) return null;
try {
if (!existsSync(path)) return null;
const raw = readFileSync(path, "utf8").trim();
if (/^[0-9a-f]{64}$/i.test(raw)) return raw.toLowerCase();
return null;
} catch { return null; }
}
export const TOKEN_FILE_ENV = ENV_TOKEN_FILE;