feat(cli): launch mints session keypair + parent attestation
claudemesh launch now also generates a per-launch ed25519 keypair and a parent-vouched attestation (12h TTL), included in the body of POST /v1/sessions/register under body.presence. The daemon stores it on SessionInfo and, with CLAUDEMESH_SESSION_PRESENCE=1, opens a long-lived broker WS so the session has its own presence row. Also fixes a latent 1.29.0 bug: claudeSessionId was referenced before its const declaration, hitting the TDZ → ReferenceError silently swallowed by the surrounding catch. Net: the IPC session-token registration has been failing every launch since 1.29.0, falling back to user-level scope for every session. Hoisted the declaration up so the registration actually runs. The presence payload is forward-compat: older daemons ignore unknown body fields, so 1.30.0 CLIs work fine against unupgraded daemons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -657,6 +657,20 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
|
|||||||
// register it with the daemon. The token's path is exposed to
|
// register it with the daemon. The token's path is exposed to
|
||||||
// the spawned claude (and all its descendants) via env so
|
// the spawned claude (and all its descendants) via env so
|
||||||
// CLI invocations from inside the session auto-attribute to it.
|
// CLI invocations from inside the session auto-attribute to it.
|
||||||
|
//
|
||||||
|
// 1.30.0: also mint an ephemeral ed25519 session keypair and a
|
||||||
|
// parent-vouched attestation. The daemon uses these to open a
|
||||||
|
// long-lived broker WebSocket per session (presence row keyed on
|
||||||
|
// the session pubkey, member_id from the parent), so sibling
|
||||||
|
// sessions in the same mesh see each other in `peer list`.
|
||||||
|
//
|
||||||
|
// Session-id resolution: 1.29.0 referenced `claudeSessionId`
|
||||||
|
// before its `const` declaration further down the file, hitting
|
||||||
|
// the TDZ → ReferenceError swallowed by the surrounding catch.
|
||||||
|
// The IPC registration has been silently failing every launch
|
||||||
|
// since 1.29.0. Hoist the declaration up so it actually runs.
|
||||||
|
const isResume = args.resume !== null || args.continueSession;
|
||||||
|
const claudeSessionId = isResume ? undefined : randomUUID();
|
||||||
let sessionTokenFilePath: string | null = null;
|
let sessionTokenFilePath: string | null = null;
|
||||||
let sessionTokenForCleanup: string | null = null;
|
let sessionTokenForCleanup: string | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -665,6 +679,45 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
|
|||||||
sessionTokenFilePath = minted.filePath;
|
sessionTokenFilePath = minted.filePath;
|
||||||
sessionTokenForCleanup = minted.token;
|
sessionTokenForCleanup = minted.token;
|
||||||
|
|
||||||
|
// Per-session ephemeral keypair + parent attestation (1.30.0+).
|
||||||
|
// Behind CLAUDEMESH_SESSION_PRESENCE: the daemon ignores the
|
||||||
|
// presence material when the flag is off, so sending it always is
|
||||||
|
// forward-compatible.
|
||||||
|
let presencePayload: {
|
||||||
|
session_pubkey: string;
|
||||||
|
session_secret_key: string;
|
||||||
|
parent_attestation: {
|
||||||
|
session_pubkey: string;
|
||||||
|
parent_member_pubkey: string;
|
||||||
|
expires_at: number;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
} | undefined;
|
||||||
|
try {
|
||||||
|
const { generateKeypair } = await import("~/services/crypto/facade.js");
|
||||||
|
const { signParentAttestation } = await import("~/services/broker/session-hello-sig.js");
|
||||||
|
const sessionKp = await generateKeypair();
|
||||||
|
const att = await signParentAttestation({
|
||||||
|
parentMemberPubkey: mesh.pubkey,
|
||||||
|
parentSecretKey: mesh.secretKey,
|
||||||
|
sessionPubkey: sessionKp.publicKey,
|
||||||
|
});
|
||||||
|
presencePayload = {
|
||||||
|
session_pubkey: sessionKp.publicKey,
|
||||||
|
session_secret_key: sessionKp.secretKey,
|
||||||
|
parent_attestation: {
|
||||||
|
session_pubkey: att.sessionPubkey,
|
||||||
|
parent_member_pubkey: att.parentMemberPubkey,
|
||||||
|
expires_at: att.expiresAt,
|
||||||
|
signature: att.signature,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Keypair / attestation failure — proceed without per-session
|
||||||
|
// presence. The session still registers; only the broker-side
|
||||||
|
// presence row is skipped.
|
||||||
|
}
|
||||||
|
|
||||||
// Register with the daemon. Best-effort: a daemon failure here
|
// Register with the daemon. Best-effort: a daemon failure here
|
||||||
// means the session falls back to user-level scope, which is fine.
|
// means the session falls back to user-level scope, which is fine.
|
||||||
const { ipc } = await import("~/daemon/ipc/client.js");
|
const { ipc } = await import("~/daemon/ipc/client.js");
|
||||||
@@ -682,6 +735,7 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
...(role ? { role } : {}),
|
...(role ? { role } : {}),
|
||||||
...(parsedGroups.length > 0 ? { groups: parsedGroups.map((g) => `@${g.name}${g.role ? `:${g.role}` : ""}`) } : {}),
|
...(parsedGroups.length > 0 ? { groups: parsedGroups.map((g) => `@${g.name}${g.role ? `:${g.role}` : ""}`) } : {}),
|
||||||
|
...(presencePayload ? { presence: presencePayload } : {}),
|
||||||
},
|
},
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
@@ -769,10 +823,8 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
|
|||||||
// passes -y / --yes. Without it, claudemesh tools still work because
|
// passes -y / --yes. Without it, claudemesh tools still work because
|
||||||
// `claudemesh install` pre-approves them via allowedTools in settings.json.
|
// `claudemesh install` pre-approves them via allowedTools in settings.json.
|
||||||
// This keeps permissions tight for multi-person meshes.
|
// This keeps permissions tight for multi-person meshes.
|
||||||
// Session identity: --resume reuses existing session, otherwise generate new.
|
// Session identity: claudeSessionId was generated above (4b) so the
|
||||||
// When resuming, Claude Code reuses the session ID so the mesh peer identity persists.
|
// session-token registration could include it. Reuse here.
|
||||||
const isResume = args.resume !== null || args.continueSession;
|
|
||||||
const claudeSessionId = isResume ? undefined : randomUUID();
|
|
||||||
|
|
||||||
const claudeArgs = [
|
const claudeArgs = [
|
||||||
"--dangerously-load-development-channels",
|
"--dangerously-load-development-channels",
|
||||||
|
|||||||
Reference in New Issue
Block a user