/** * c2c identity for a pi session. * * A pi session self-registers with the c2c broker on startup — no `c2c start` * supervisor is involved. We need two things: * * - a c2c **session id**: an opaque key the broker uses to route the inbox. * We derive it from pi's own session id, namespaced `pi-` so pi sessions * are identifiable in `c2c list`. * - an **alias**: the human-facing peer name. The live swarm uses word-pair * aliases (e.g. `lyra-quill`) assigned by the supervisor with collision * avoidance. Since we self-register, we default to a deterministic * `pi-` alias which is collision-safe and recognizable. An operator * can override it via `C2C_PI_ALIAS`. */ import { createHash } from "node:crypto"; import type { C2cCli, C2cWhoami } from "./c2c-cli.ts"; const SESSION_PREFIX = "pi-"; const ALIAS_PREFIX = "pi-"; const ALIAS_HASH_LEN = 6; const SUBAGENT_SUFFIX_LEN = 1 + 1 + ALIAS_HASH_LEN; // -a /** * Max parent-alias length preserved in a subagent alias. `deriveSubagentAlias` * truncates the parent to this many chars before appending `-a` (so the * whole alias fits in 64). Consumers that recover the parent from a subagent * alias (e.g. the peer-tree builder) can only match parents up to this length. */ export const SUBAGENT_PARENT_BASE_MAX = Math.max(1, 64 - SUBAGENT_SUFFIX_LEN); // 56 /** Strip an alias to broker-safe characters. Empty if nothing survives. */ export function sanitizeAlias(raw: string): string { return raw .trim() .replace(/[^A-Za-z0-9._-]/g, "-") .replace(/-+/g, "-") .replace(/^[-.]+|[-.]+$/g, "") .slice(0, 64); } /** * Derive a stable c2c session id from pi's session id. When pi gives us no * session id (rare; non-TUI modes), fall back to a caller-supplied value so * the result is deterministic and testable. */ export function deriveSessionId(piSessionId: string | null | undefined, fallback = "default"): string { const base = (piSessionId ?? "").trim() || fallback; return base.startsWith(SESSION_PREFIX) ? base : `${SESSION_PREFIX}${base}`; } /** * Resolve the alias to register. Priority: * 1. an explicitly configured alias (sanitized), if non-empty; * 2. a deterministic `pi-` derived from the session id. */ export function resolveAlias(opts: { configured?: string | null; sessionId: string }): string { const configured = opts.configured ? sanitizeAlias(opts.configured) : ""; if (configured) return configured; const hash = createHash("sha256").update(opts.sessionId).digest("hex").slice(0, ALIAS_HASH_LEN); return `${ALIAS_PREFIX}${hash}`; } export function deriveSubagentAlias(opts: { parentAlias: string; agentId?: string | null; sessionId: string; }): string { const seed = (opts.agentId ?? "").trim() || opts.sessionId; const hash = createHash("sha256").update(seed).digest("hex").slice(0, ALIAS_HASH_LEN); const fallbackParent = resolveAlias({ sessionId: opts.sessionId }); const parent = sanitizeAlias(opts.parentAlias) || fallbackParent; const base = parent.slice(0, SUBAGENT_PARENT_BASE_MAX); return `${base}-a${hash}`; } export interface IdentityInputs { /** pi's session id (ctx.sessionManager.getSessionId()). */ piSessionId: string | null | undefined; /** Operator override (C2C_PI_ALIAS). */ configuredAlias?: string | null; /** Fallback session base when pi has no session id. */ fallbackSessionId?: string; /** * Ambient C2C_MCP_SESSION_ID, if already set in the environment (e.g. a * future `c2c start pi` supervisor). When present and non-empty it is used * verbatim as the session id — no `pi-` prefix — so the extension binds to * the session the host already established instead of inventing a new one. */ sessionIdEnv?: string | null; subagent?: { depth: number; agentId?: string; parentAlias?: string | null; } | null; } export interface Identity { alias: string; sessionId: string; } /** Compute the identity (pure) without touching the broker. */ export function computeIdentity(inputs: IdentityInputs): Identity { if (inputs.subagent && inputs.subagent.depth > 0) { const sessionId = deriveSessionId(inputs.piSessionId, inputs.fallbackSessionId); const hintedParent = inputs.subagent.parentAlias ?? sanitizeAlias(inputs.configuredAlias ?? ""); const parentAlias = hintedParent || resolveAlias({ sessionId }); const alias = deriveSubagentAlias({ parentAlias, agentId: inputs.subagent.agentId, sessionId, }); return { alias, sessionId }; } const ambient = inputs.sessionIdEnv?.trim(); const sessionId = ambient ? ambient : deriveSessionId(inputs.piSessionId, inputs.fallbackSessionId); const alias = resolveAlias({ configured: inputs.configuredAlias, sessionId }); return { alias, sessionId }; } export interface EstablishResult { ok: boolean; identity: Identity; /** whoami echo from the broker on success. */ whoami?: C2cWhoami | null; /** Error message on failure (registration is best-effort; never throws). */ error?: string; } /** * Register this session's identity with the broker. Best-effort: on failure it * returns `ok:false` with the error rather than throwing, so a broker hiccup * never crashes pi's `session_start`. On success the `cli` is left scoped to * the registered session id. */ export async function establishIdentity(cli: C2cCli, inputs: IdentityInputs): Promise { const identity = computeIdentity(inputs); try { const whoami = await cli.register(identity.alias, identity.sessionId); return { ok: true, identity, whoami }; } catch (err) { return { ok: false, identity, error: err instanceof Error ? err.message : String(err) }; } }