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>
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
import { request as httpRequest } from "node:http";
|
|
|
|
import { DAEMON_PATHS, DAEMON_TCP_HOST, DAEMON_TCP_DEFAULT_PORT } from "../paths.js";
|
|
import { readLocalToken } from "../local-token.js";
|
|
import { readSessionTokenFromEnv } from "~/services/session/token.js";
|
|
|
|
export interface IpcRequestOptions {
|
|
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
|
path: string;
|
|
body?: unknown;
|
|
/** Force TCP loopback instead of UDS (for tests / cross-container scenarios). */
|
|
preferTcp?: boolean;
|
|
timeoutMs?: number;
|
|
}
|
|
|
|
export interface IpcResponse<T = unknown> {
|
|
status: number;
|
|
body: T;
|
|
}
|
|
|
|
export class IpcError extends Error {
|
|
constructor(public status: number, public payload: unknown, msg: string) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
/** Small, dependency-free IPC client for talking to the local daemon. */
|
|
export async function ipc<T = unknown>(opts: IpcRequestOptions): Promise<IpcResponse<T>> {
|
|
const useTcp = !!opts.preferTcp;
|
|
const headers: Record<string, string> = {
|
|
accept: "application/json",
|
|
host: "localhost",
|
|
};
|
|
|
|
let bodyBuf: Buffer | undefined;
|
|
if (opts.body !== undefined) {
|
|
bodyBuf = Buffer.from(JSON.stringify(opts.body), "utf8");
|
|
headers["content-type"] = "application/json";
|
|
headers["content-length"] = String(bodyBuf.length);
|
|
}
|
|
|
|
if (useTcp) {
|
|
const tok = readLocalToken();
|
|
if (!tok) throw new IpcError(0, null, "daemon local token not found; is the daemon running?");
|
|
headers.authorization = `Bearer ${tok}`;
|
|
}
|
|
|
|
// Per-session token attribution. When the calling process has
|
|
// CLAUDEMESH_IPC_TOKEN_FILE set (a launched session and its
|
|
// descendants), attach the session token. The daemon's auth
|
|
// middleware resolves it to a SessionInfo and uses it for default-
|
|
// mesh scoping. Sent as a second Authorization header is not
|
|
// possible per HTTP semantics, so we layer: when both UDS and a
|
|
// session token exist, send the session token; the bearer remains
|
|
// only for TCP loopback callers.
|
|
if (!useTcp) {
|
|
const sessionTok = readSessionTokenFromEnv();
|
|
if (sessionTok) headers.authorization = `ClaudeMesh-Session ${sessionTok}`;
|
|
}
|
|
|
|
return new Promise<IpcResponse<T>>((resolve, reject) => {
|
|
const req = httpRequest(
|
|
useTcp
|
|
? { host: DAEMON_TCP_HOST, port: DAEMON_TCP_DEFAULT_PORT, path: opts.path, method: opts.method ?? "GET", headers }
|
|
: { socketPath: DAEMON_PATHS.SOCK_FILE, path: opts.path, method: opts.method ?? "GET", headers },
|
|
(res) => {
|
|
const chunks: Buffer[] = [];
|
|
res.on("data", (c) => chunks.push(c));
|
|
res.on("end", () => {
|
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
let parsed: unknown = raw;
|
|
try { parsed = raw.length > 0 ? JSON.parse(raw) : null; } catch { /* leave raw */ }
|
|
resolve({ status: res.statusCode ?? 0, body: parsed as T });
|
|
});
|
|
},
|
|
);
|
|
req.setTimeout(opts.timeoutMs ?? 5_000, () => req.destroy(new Error("ipc_timeout")));
|
|
req.on("error", (err) => reject(err));
|
|
if (bodyBuf) req.write(bodyBuf);
|
|
req.end();
|
|
});
|
|
}
|