UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

1,259 lines (1,214 loc) • 47 kB
import { createHash, randomUUID, timingSafeEqual } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { HubClient, HubHttpError } from "./client.ts"; import { IMPROVEMENT_AREAS } from "./protocol.ts"; import { JOURNAL_CATEGORIES } from "./workflow.ts"; import type { DeliveryMode, ImprovementArea, JournalCategory, MessageRecord, WorkflowCheckpointStatus, WorkflowEvidenceInput, WorkflowEvidenceReferenceInput, } from "./protocol.ts"; export interface CommandSchemaProperty { type: "string" | "number" | "integer" | "boolean" | "object" | "array"; description?: string; enum?: readonly string[]; default?: unknown; minimum?: number; maximum?: number; minItems?: number; maxItems?: number; maxProperties?: number; items?: Record<string, unknown>; patternProperties?: Record<string, unknown>; properties?: Record<string, unknown>; required?: readonly string[]; additionalProperties?: boolean | Record<string, unknown>; } export interface CommandJsonSchema { type: "object"; properties: Record<string, CommandSchemaProperty>; required?: readonly string[]; additionalProperties?: boolean; } export interface ToolPolicy { preset?: string; allow?: string[]; deny?: string[]; allowedTools?: string[]; deniedTools?: string[]; } export interface AttemptTokenPayload { schema: "kxm.attempt-token.v1"; runId: string; stepId: string; stepAttempt: number; assignmentId: string; attemptId: string; agentId: string; stageId?: string; attempt?: number; toolPolicy?: ToolPolicy; issuedAt?: string; } export interface SessionTokenPayload { schema: "kxm.session-token.v1"; sessionId: string; agentName?: string; toolPolicy?: ToolPolicy; issuedAt: string; expiresAt?: string; } export interface CommandExecutionContext { signal?: AbortSignal | undefined; /** An event-fed inbox the caller keeps (the MCP server); `kxm_inbox` reconciles it. */ inbox?: Map<string, MessageRecord> | undefined; notifiedInbox?: Set<string> | undefined; /** The caller keeps no inbox and reads its open requests from the hub (a one-shot CLI * call). A caller with neither this nor `inbox` activates inbound requests itself (Pi). */ hubInbox?: boolean | undefined; /** Inbound requests this session is handling: the Pi extension's active request, the MCP * server's open inbox. A request sent meanwhile is one more hop along their chain. */ handling?: readonly Pick<MessageRecord, "hops" | "maxHops">[] | undefined; } /** * Hop fields for a request sent while handling inbound work: one hop past the furthest * handled request, under the tightest limit among them, so the hub's `hop_limit_reached` * refusal bounds a chain of agents forwarding to each other. Handling nothing starts a new * chain with the hub defaults. When several requests are open the furthest one counts, so a * forwarding loop cannot reset its count because an unrelated request arrived beside it. */ export function forwardedHops( handling: CommandExecutionContext["handling"], ): { hops: number; maxHops: number } | undefined { if (!handling?.length) return undefined; return { hops: Math.max(...handling.map((message) => message.hops)) + 1, maxHops: Math.min(...handling.map((message) => message.maxHops)), }; } export interface AgentCommand { /** Tool name exposed in MCP and Pi, e.g. "kxm_send" */ name: string; /** Primary CLI group, e.g. "peer" or "workflow" or "context" */ group: "peer" | "workflow" | "context"; /** Subcommand verb in CLI, e.g. "send" or "checkpoint" */ verb: string; /** Label for UI displays */ label: string; /** Human-readable and LLM description */ description: string; /** JSON Schema parameters */ parameters: CommandJsonSchema; /** Shared implementation over HubClient */ execute: ( client: HubClient, args: Record<string, unknown>, context?: CommandExecutionContext, ) => Promise<unknown>; } function requiredString(value: unknown, name: string): string { if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} is required`); return value.trim(); } function optionalString(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value.trim() : undefined; } function asRecord(value: unknown): Record<string, unknown> { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {}; } function optionalWorkflowContext( value: unknown, ): { runId: string; stageId: string; requirementKey: string; attempt: number } | undefined { if (value === undefined) return undefined; const context = asRecord(value); if ( !Number.isInteger(context.attempt) || (context.attempt as number) < 1 || (context.attempt as number) > 20 ) { throw new Error("workflowContext.attempt must be an integer between 1 and 20"); } return { runId: requiredString(context.runId, "workflowContext.runId"), stageId: requiredString(context.stageId, "workflowContext.stageId"), requirementKey: requiredString(context.requirementKey, "workflowContext.requirementKey"), attempt: context.attempt as number, }; } function optionalEvidenceRefs(value: unknown): WorkflowEvidenceReferenceInput | undefined { return value && typeof value === "object" && !Array.isArray(value) ? (value as WorkflowEvidenceReferenceInput) : undefined; } function isTerminalMessageError(error: unknown): boolean { return ( error instanceof HubHttpError && (error.statusCode === 409 || (error.statusCode === 404 && error.code === "message_not_found")) ); } function isTerminalMessage(message: MessageRecord): boolean { return ( message.status === "replied" || message.status === "cancelled" || message.status === "expired" || message.status === "error" ); } export async function reconcileInbox( client: HubClient, inbox: Map<string, MessageRecord>, notifiedInbox?: Set<string>, ): Promise<void> { await Promise.all( [...inbox.keys()].map(async (messageId) => { try { const current = await client.getMessage(messageId); if (isTerminalMessage(current)) { inbox.delete(messageId); notifiedInbox?.delete(messageId); } else { inbox.set(messageId, current); } } catch (error) { if (isTerminalMessageError(error)) { inbox.delete(messageId); notifiedInbox?.delete(messageId); return; } throw error; } }), ); } function resolveProject(client: HubClient, projectArg: unknown): string { const proj = optionalString(projectArg) ?? client.agent?.project; if (!proj) { throw new Error('missing required parameter "project"'); } return proj; } export const AGENT_COMMANDS: readonly AgentCommand[] = [ { name: "kxm_list", group: "peer", verb: "list", label: "List hub peers", description: "List peer agents in this project's hub pool with their names, purposes, host label, and hub-clocked presence (online, stale, offline). Registered offline peers are listed only when includeOffline is set.", parameters: { type: "object", properties: { includeOffline: { type: "boolean", description: "Also list registered peers whose hub lease has expired", }, }, additionalProperties: false, }, async execute(client, args) { return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) }; }, }, { name: "kxm_send", group: "peer", verb: "send", label: "Send peer request", description: "Send a focused request to a peer agent. Returns a message ID for kxm_get or kxm_await. For durable peer evidence, workflowContext is the hub-authorized provenance scope; correlation and idempotency are transport concerns and do not establish evidence provenance.", parameters: { type: "object", properties: { target: { type: "string", description: "Peer name or agent ID" }, content: { type: "string", description: "Focused request with the expected response or artifact" }, delivery: { type: "string", enum: ["steer", "followUp", "nextTurn"], default: "followUp", description: "followUp is the safe default; use steer only for active blockers", }, correlationId: { type: "string", description: "Optional task grouping; the hub binds it to runId when workflowContext is supplied", }, idempotencyKey: { type: "string", description: "Retry/deduplication key only; not a workflow security or evidence binding", }, workflowContext: { type: "object", description: "Requested provenance scope; the hub authorizes and persists the canonical binding", properties: { runId: { type: "string", description: "Active durable workflow run ID" }, stageId: { type: "string", description: "Active workflow stage ID" }, requirementKey: { type: "string", description: "Required evidence identity this peer reply may satisfy" }, attempt: { type: "integer", minimum: 1, maximum: 20, description: "Current one-based stage attempt" }, }, required: ["runId", "stageId", "requirementKey", "attempt"], additionalProperties: false, }, ttlMs: { type: "number", minimum: 1_000, maximum: 604_800_000, description: "Message TTL in milliseconds" }, allowOffline: { type: "boolean", default: false, description: "Queue the request if the target is a registered offline agent in this project", }, }, required: ["target", "content"], additionalProperties: false, }, async execute(client, args, context) { const delivery = optionalString(args.delivery) as DeliveryMode | undefined; const correlationId = optionalString(args.correlationId); const idempotencyKey = optionalString(args.idempotencyKey); const workflowContext = optionalWorkflowContext(args.workflowContext); const message = await client.send({ ...forwardedHops(context?.handling), target: requiredString(args.target, "target"), content: requiredString(args.content, "content"), ...(delivery ? { delivery } : {}), ...(correlationId ? { correlationId } : {}), ...(idempotencyKey ? { idempotencyKey } : {}), ...(workflowContext ? { workflowContext } : {}), ...(typeof args.ttlMs === "number" ? { ttlMs: args.ttlMs } : {}), ...(args.allowOffline === true ? { allowOffline: true } : {}), }); return { messageId: message.id, status: message.status, target: message.toName }; }, }, { name: "kxm_get", group: "peer", verb: "get", label: "Get peer request", description: "Check the status and optional reply for a previously sent request.", parameters: { type: "object", properties: { messageId: { type: "string", description: "Message ID of the sent request" }, }, required: ["messageId"], additionalProperties: false, }, async execute(client, args) { return await client.getMessage(requiredString(args.messageId, "messageId")); }, }, { name: "kxm_fanout", group: "peer", verb: "fanout", label: "Fanout peer requests", description: "Ask one through three peers independently and return replies for comparison and synthesis. A local timeout or request cancellation returns a pending response with a durable messageId for kxm_get or an exact retry.", parameters: { type: "object", properties: { targets: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 3, description: "One through three target peer names or agent IDs", }, content: { type: "string", description: "Task description sent to all targets" }, correlationId: { type: "string", description: "Optional task grouping; the hub binds it to runId when workflowContext is supplied", }, idempotencyKeyPrefix: { type: "string", description: "Stable retry/deduplication prefix only; not a workflow security or evidence binding", }, workflowContext: { type: "object", description: "Requested provenance scope shared by each request", properties: { runId: { type: "string", description: "Active durable workflow run ID" }, stageId: { type: "string", description: "Active workflow stage ID" }, requirementKey: { type: "string", description: "Required evidence identity these peer replies may satisfy" }, attempt: { type: "integer", minimum: 1, maximum: 20, description: "Current one-based stage attempt" }, }, required: ["runId", "stageId", "requirementKey", "attempt"], additionalProperties: false, }, ttlMs: { type: "number", minimum: 1_000, maximum: 604_800_000, description: "Message TTL in milliseconds" }, timeoutMs: { type: "number", minimum: 100, maximum: 1_800_000, description: "Client wait timeout in milliseconds" }, }, required: ["targets", "content"], additionalProperties: false, }, async execute(client, args, context) { const targets = Array.isArray(args.targets) ? args.targets.map((t) => requiredString(t, "target")) : []; return { responses: await client.fanout({ ...forwardedHops(context?.handling), targets, content: requiredString(args.content, "content"), ...(optionalString(args.correlationId) ? { correlationId: optionalString(args.correlationId)! } : {}), ...(optionalString(args.idempotencyKeyPrefix) ? { idempotencyKeyPrefix: optionalString(args.idempotencyKeyPrefix)! } : {}), ...(optionalWorkflowContext(args.workflowContext) ? { workflowContext: optionalWorkflowContext(args.workflowContext)! } : {}), ...(typeof args.ttlMs === "number" ? { ttlMs: args.ttlMs } : {}), ...(typeof args.timeoutMs === "number" ? { timeoutMs: args.timeoutMs } : {}), ...(context?.signal ? { signal: context.signal } : {}), }), }; }, }, { name: "kxm_await", group: "peer", verb: "await", label: "Await peer response", description: "Wait until a sent request receives a reply or reaches a terminal error. Capped at 60 seconds (60000ms); longer waits are workflow wait steps.", parameters: { type: "object", properties: { messageId: { type: "string", description: "Message ID of the sent request" }, timeoutMs: { type: "number", minimum: 100, maximum: 60_000, default: 60_000, description: "Timeout in milliseconds (capped at 60 seconds)", }, }, required: ["messageId"], additionalProperties: false, }, async execute(client, args, context) { const timeoutMs = Math.min( typeof args.timeoutMs === "number" ? args.timeoutMs : 60_000, 60_000, ); return await client.awaitResponse( requiredString(args.messageId, "messageId"), timeoutMs, context?.signal, ); }, }, { name: "kxm_cancel", group: "peer", verb: "cancel", label: "Cancel peer request", description: "Cancel a queued or delivered request sent by this agent.", parameters: { type: "object", properties: { messageId: { type: "string", description: "Message ID of the request to cancel" }, }, required: ["messageId"], additionalProperties: false, }, async execute(client, args) { return await client.cancel(requiredString(args.messageId, "messageId")); }, }, { name: "kxm_inbox", group: "peer", verb: "inbox", label: "List inbound requests", description: "List inbound peer requests awaiting a reply.", parameters: { type: "object", properties: {}, additionalProperties: false, }, async execute(client, _args, context) { if (context?.inbox) { await reconcileInbox(client, context.inbox, context.notifiedInbox); return { messages: [...context.inbox.values()] }; } if (context?.hubInbox) return { messages: await client.listInbox() }; // Listing would offer requests this session's own activation queue is about to hand // it as turns, and an empty list would hide them, so it refuses. throw new Error( "kxm_inbox is not available in this session: it activates each inbound request as a turn, and that turn's final response is the reply", ); }, }, { name: "kxm_reply", group: "peer", verb: "reply", label: "Reply to peer request", description: "Reply to an inbound peer request using its message ID.", parameters: { type: "object", properties: { messageId: { type: "string", description: "Message ID of the inbound request" }, content: { type: "string", description: "Final response with evidence and remaining risks" }, }, required: ["messageId", "content"], additionalProperties: false, }, async execute(client, args, context) { const messageId = requiredString(args.messageId, "messageId"); try { const message = await client.reply(messageId, requiredString(args.content, "content")); if (context?.inbox) { context.inbox.delete(messageId); context.notifiedInbox?.delete(messageId); } return { messageId, status: message.status, recipient: message.fromName }; } catch (error) { if (context?.inbox && isTerminalMessageError(error)) { context.inbox.delete(messageId); context.notifiedInbox?.delete(messageId); } throw error; } }, }, { name: "kxm_workflow_list", group: "workflow", verb: "runs", label: "List workflow runs", description: "List durable webhook workflows assigned to this agent.", parameters: { type: "object", properties: {}, additionalProperties: false, }, async execute(client) { return { runs: await client.listWorkflows() }; }, }, { name: "kxm_workflow_get", group: "workflow", verb: "run", label: "Get workflow run", description: "Get a workflow's stages and its learning journal (plans, decisions, contradictions, errors, lessons, and the other journal categories).", parameters: { type: "object", properties: { runId: { type: "string", description: "Workflow run ID" }, }, required: ["runId"], additionalProperties: false, }, async execute(client, args) { return await client.getWorkflow(requiredString(args.runId, "runId")); }, }, { name: "kxm_workflow_checkpoint", group: "workflow", verb: "checkpoint", label: "Checkpoint workflow stage", description: "Record a stage result with evidence keyed by the stage's required evidence identities. Cite peer-reply requirements through evidenceRefs so the hub can verify provenance and quorum; caller-authored evidence strings cannot satisfy those policies. Warnings and failures require another attempt until passed or exhausted.", parameters: { type: "object", properties: { runId: { type: "string", description: "Active durable workflow run ID" }, stageId: { type: "string", description: "Active stage ID" }, status: { type: "string", enum: ["passed", "warning", "failed"], description: "Stage outcome" }, summary: { type: "string", description: "Summary of changes, verification, and remaining risks" }, evidence: { type: "object", additionalProperties: { type: "string" }, maxProperties: 64, description: "Key-value evidence mapping required keys to proof strings", }, evidenceRefs: { type: "object", description: "Peer evidence references keyed by required evidence identity", patternProperties: { "^(.*)$": { type: "object", properties: { messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }, }, required: ["messageIds"], additionalProperties: false, }, }, additionalProperties: { type: "object", properties: { messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }, }, required: ["messageIds"], additionalProperties: false, }, maxProperties: 32, }, }, required: ["runId", "stageId", "status", "summary"], additionalProperties: false, }, async execute(client, args) { return await client.checkpointWorkflow(requiredString(args.runId, "runId"), { stageId: requiredString(args.stageId, "stageId"), status: requiredString(args.status, "status") as WorkflowCheckpointStatus, summary: requiredString(args.summary, "summary"), ...(args.evidence && typeof args.evidence === "object" && !Array.isArray(args.evidence) ? { evidence: args.evidence as WorkflowEvidenceInput } : {}), ...(optionalEvidenceRefs(args.evidenceRefs) ? { evidenceRefs: optionalEvidenceRefs(args.evidenceRefs)! } : {}), }); }, }, { name: "kxm_workflow_record", group: "workflow", verb: "record", label: "Record workflow journal entry", description: "Record a plan, decision, contradiction, error, lesson, observation, hypothesis, experiment, state-change, or skill-candidate for continuous improvement. Pass stageId to bind the entry to that stage: the hub derives the attempt, and area defaults to the stage's declared area. Lessons and skill-candidates require evidence.", parameters: { type: "object", properties: { runId: { type: "string", description: "Active durable workflow run ID" }, category: { type: "string", enum: [...JOURNAL_CATEGORIES], description: "Category of journal entry", }, area: { type: "string", enum: [...IMPROVEMENT_AREAS], description: "System area; required unless stageId names a stage that declares an area", }, stageId: { type: "string", description: "Stage the entry belongs to; the hub binds the attempt from the stage's state", }, severity: { type: "string", enum: ["info", "warning", "error"], default: "info", description: "Severity level", }, summary: { type: "string", description: "Concise description of the observation or decision" }, details: { type: "string", description: "Extended details, context, and reasoning" }, evidence: { type: "array", items: { type: "string" }, maxItems: 32, description: "Durable evidence strings or URIs", }, relatedEntryIds: { type: "array", items: { type: "string" }, maxItems: 16, description: "Related previous journal entry IDs", }, }, required: ["runId", "category", "summary"], additionalProperties: false, }, async execute(client, args) { return await client.recordWorkflowEntry(requiredString(args.runId, "runId"), { category: requiredString(args.category, "category") as JournalCategory, ...(optionalString(args.area) ? { area: optionalString(args.area) as ImprovementArea } : {}), ...(optionalString(args.stageId) ? { stageId: optionalString(args.stageId)! } : {}), ...(optionalString(args.severity) ? { severity: optionalString(args.severity) as "info" | "warning" | "error" } : {}), summary: requiredString(args.summary, "summary"), ...(optionalString(args.details) ? { details: optionalString(args.details)! } : {}), ...(Array.isArray(args.evidence) ? { evidence: args.evidence as string[] } : {}), ...(Array.isArray(args.relatedEntryIds) ? { relatedEntryIds: args.relatedEntryIds as string[] } : {}), }); }, }, { name: "kxm_workflow_wait", group: "workflow", verb: "wait", label: "Wait for workflow signal", description: "Pause the active stage until a signed external callback checkpoints it and resumes the coordinator. Cite peer-reply requirements through evidenceRefs so the hub can verify provenance and quorum; verified evidence is accumulated with callback evidence.", parameters: { type: "object", properties: { runId: { type: "string", description: "Active durable workflow run ID" }, stageId: { type: "string", description: "Active stage ID" }, signalKey: { type: "string", description: "Stable callback key, such as github-pr-42-checks" }, summary: { type: "string", description: "What is running externally and what result is expected" }, evidence: { type: "object", additionalProperties: { type: "string" }, maxProperties: 64, description: "Evidence gathered before the wait", }, evidenceRefs: { type: "object", description: "Peer evidence references verified before waiting", patternProperties: { "^(.*)$": { type: "object", properties: { messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }, }, required: ["messageIds"], additionalProperties: false, }, }, additionalProperties: { type: "object", properties: { messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }, }, required: ["messageIds"], additionalProperties: false, }, maxProperties: 32, }, timeoutMs: { type: "number", minimum: 1_000, maximum: 2_592_000_000, description: "Maximum wait duration in milliseconds", }, }, required: ["runId", "stageId", "signalKey", "summary"], additionalProperties: false, }, async execute(client, args) { return await client.waitForWorkflowSignal(requiredString(args.runId, "runId"), { stageId: requiredString(args.stageId, "stageId"), signalKey: requiredString(args.signalKey, "signalKey"), summary: requiredString(args.summary, "summary"), ...(args.evidence && typeof args.evidence === "object" && !Array.isArray(args.evidence) ? { evidence: args.evidence as WorkflowEvidenceInput } : {}), ...(optionalEvidenceRefs(args.evidenceRefs) ? { evidenceRefs: optionalEvidenceRefs(args.evidenceRefs)! } : {}), ...(typeof args.timeoutMs === "number" ? { timeoutMs: args.timeoutMs } : {}), }); }, }, { name: "kxm_improvement_report", group: "workflow", verb: "improve-report", label: "Summarize improvement report", description: "Summarize workflow errors, contradictions, lessons, and skill candidates by improvement area, plus ranked cross-run signals: duplicates merged across runs and scored by frequency x severity x run-attempt cost x evidence confidence, security first, with redacted text.", parameters: { type: "object", properties: {}, additionalProperties: false, }, async execute(client) { return await client.improvementReport(); }, }, { name: "kxm_context", group: "context", verb: "get", label: "Get KXM context packet", description: "Normal entry point for KXM context. Assembles a token-budgeted role-aware context packet from durable journal evidence, temporal state, knowledge, episodes, and skills. Superseded and rejected records are excluded. Use KXM context tools instead of provider-specific memory APIs.", parameters: { type: "object", properties: { project: { type: "string", description: "Project scope (must be the client's project)" }, role: { type: "string", description: "Requesting role: repro, planner, critic, implementer, verifier, or custom", }, task: { type: "string", description: "What the role is trying to accomplish" }, workflowRunId: { type: "string", description: "Workflow run scope" }, stageId: { type: "string", description: "Workflow stage scope" }, budgetTokens: { type: "integer", description: "Token budget; defaults to the role policy" }, includeKinds: { type: "array", items: { type: "string", enum: ["evidence", "state", "episode", "knowledge", "skill"] }, description: "Restrict packet to these item kinds", }, }, required: ["role", "task"], additionalProperties: false, }, async execute(client, args) { return await client.contextGet({ project: resolveProject(client, args.project), role: requiredString(args.role, "role"), task: requiredString(args.task, "task"), ...(optionalString(args.workflowRunId) ? { workflowRunId: optionalString(args.workflowRunId)! } : {}), ...(optionalString(args.stageId) ? { stageId: optionalString(args.stageId)! } : {}), ...(typeof args.budgetTokens === "number" ? { budgetTokens: args.budgetTokens } : {}), ...(Array.isArray(args.includeKinds) ? { includeKinds: args.includeKinds as any } : {}), }); }, }, { name: "kxm_recall", group: "context", verb: "recall", label: "Recall context metadata", description: "Search durable context records for a project by query. Ranks exact-phrase matches first, then token relevance, then id; returns bounded metadata with a numeric relevance per item, never summaries.", parameters: { type: "object", properties: { project: { type: "string", description: "Project identifier" }, query: { type: "string", description: "Query string" }, kinds: { type: "array", items: { type: "string" }, description: "Kinds filter" }, limit: { type: "integer", minimum: 1, maximum: 100, description: "Maximum results" }, }, additionalProperties: false, }, async execute(client, args) { return await client.contextRecall({ project: resolveProject(client, args.project), ...(optionalString(args.query) ? { query: optionalString(args.query)! } : {}), ...(Array.isArray(args.kinds) ? { kinds: args.kinds as any } : {}), ...(typeof args.limit === "number" ? { limit: args.limit } : {}), }); }, }, { name: "kxm_state", group: "context", verb: "state", label: "Get temporal state", description: "Current value for one temporal state key, optionally as of a historical timestamp.", parameters: { type: "object", properties: { project: { type: "string", description: "Project identifier" }, key: { type: "string", description: "State key" }, asOf: { type: "string", description: "ISO-8601 timestamp for historical queries" }, }, required: ["key"], additionalProperties: false, }, async execute(client, args) { return await client.contextState({ project: resolveProject(client, args.project), key: requiredString(args.key, "key"), ...(optionalString(args.asOf) ? { asOf: optionalString(args.asOf)! } : {}), }); }, }, { name: "kxm_episode", group: "context", verb: "episode", label: "Get workflow episodes", description: "Episodic learning from workflow journals: errors, lessons, observations, experiments for a project.", parameters: { type: "object", properties: { project: { type: "string", description: "Project identifier" }, workflowRunId: { type: "string", description: "Optional workflow run scope" }, }, additionalProperties: false, }, async execute(client, args) { return await client.contextEpisode({ project: resolveProject(client, args.project), ...(optionalString(args.workflowRunId) ? { workflowRunId: optionalString(args.workflowRunId)! } : {}), }); }, }, { name: "kxm_promote", group: "context", verb: "promote", label: "Propose state promotion", description: "Propose a change to one authoritative state key. Promotion requires durable evidence.", parameters: { type: "object", properties: { project: { type: "string", description: "Project identifier" }, key: { type: "string", description: "State key to promote" }, summary: { type: "string", description: "Promotion summary" }, authority: { type: "string", enum: ["evidence", "hypothesis"], description: "Authority class; an agent's proposal is peer origin, so evidence at most", }, confidence: { type: "string", enum: ["verified", "probable", "uncertain"], description: "Confidence level", }, evidenceRefs: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 32, description: "Evidence item references backing the promotion", }, }, required: ["key", "summary", "authority", "confidence", "evidenceRefs"], additionalProperties: false, }, async execute(client, args) { return await client.contextStatePropose({ project: resolveProject(client, args.project), key: requiredString(args.key, "key"), summary: requiredString(args.summary, "summary"), authority: requiredString(args.authority, "authority") as any, confidence: requiredString(args.confidence, "confidence") as any, evidenceRefs: Array.isArray(args.evidenceRefs) ? (args.evidenceRefs as string[]) : [], }); }, }, ]; export const AGENT_COMMANDS_MAP: ReadonlyMap<string, AgentCommand> = new Map( AGENT_COMMANDS.map((cmd) => [cmd.name, cmd]), ); export function getMcpTools(): Array<{ name: string; description: string; inputSchema: Record<string, unknown>; }> { return AGENT_COMMANDS.map((cmd) => ({ name: cmd.name, description: cmd.description, inputSchema: cmd.parameters as unknown as Record<string, unknown>, })); } export function getPiToolDefinitions(): Array<{ name: string; label: string; description: string; parameters: Record<string, unknown>; }> { return AGENT_COMMANDS.map((cmd) => ({ name: cmd.name, label: cmd.label, description: cmd.description, parameters: cmd.parameters as unknown as Record<string, unknown>, })); } export function getCliAgentCommands(): readonly AgentCommand[] { return AGENT_COMMANDS; } export function mintAttemptToken(input: { runId: string; stepId?: string; stepAttempt?: number; assignmentId?: string; attemptId?: string; agentId?: string; toolPolicy?: ToolPolicy; stageId?: string; attempt?: number; allowedTools?: string[]; deniedTools?: string[]; preset?: string; }): string { const toolPolicy: ToolPolicy | undefined = input.toolPolicy ?? ( input.allowedTools || input.deniedTools || input.preset ? { ...(input.preset ? { preset: input.preset } : {}), ...(input.allowedTools ? { allow: input.allowedTools, allowedTools: input.allowedTools } : {}), ...(input.deniedTools ? { deny: input.deniedTools, deniedTools: input.deniedTools } : {}), } : undefined ); const payload: AttemptTokenPayload = { schema: "kxm.attempt-token.v1", runId: input.runId, stepId: input.stepId ?? input.stageId ?? "step-1", stepAttempt: input.stepAttempt ?? input.attempt ?? 1, stageId: input.stageId ?? input.stepId ?? "step-1", attempt: input.attempt ?? input.stepAttempt ?? 1, assignmentId: input.assignmentId ?? `assign-${randomUUID()}`, attemptId: input.attemptId ?? `attempt-${randomUUID()}`, agentId: input.agentId ?? "agent-worker", issuedAt: new Date().toISOString(), ...(toolPolicy ? { toolPolicy } : {}), }; return Buffer.from(JSON.stringify(payload)).toString("base64url"); } export function parseAttemptToken(token: string): AttemptTokenPayload | undefined { try { const raw = Buffer.from(token.trim(), "base64url").toString("utf8"); const parsed = JSON.parse(raw); if ( parsed && typeof parsed === "object" && parsed.schema === "kxm.attempt-token.v1" && typeof parsed.runId === "string" ) { return parsed as AttemptTokenPayload; } } catch { return undefined; } return undefined; } export function timingSafeStringCompare(a: string | undefined, b: string | undefined): boolean { if (typeof a !== "string" || typeof b !== "string") return false; const hashA = createHash("sha256").update(a).digest(); const hashB = createHash("sha256").update(b).digest(); return timingSafeEqual(hashA, hashB); } export function mintSessionToken(input?: { sessionId?: string; agentName?: string; toolPolicy?: ToolPolicy; preset?: string; allowedTools?: string[]; deniedTools?: string[]; expiresAt?: string; ttlMs?: number; }): string { const toolPolicy: ToolPolicy | undefined = input?.toolPolicy ?? ( input?.allowedTools || input?.deniedTools || input?.preset ? { ...(input?.preset ? { preset: input.preset } : {}), ...(input?.allowedTools ? { allow: input.allowedTools, allowedTools: input.allowedTools } : {}), ...(input?.deniedTools ? { deny: input.deniedTools, deniedTools: input.deniedTools } : {}), } : undefined ); const issuedAt = new Date().toISOString(); // Decision Q2: Default 24-hour TTL for disk SessionToken const defaultTtlMs = 24 * 60 * 60 * 1000; const ttlMs = typeof input?.ttlMs === "number" && input.ttlMs > 0 ? input.ttlMs : defaultTtlMs; const expiresAt = input?.expiresAt ?? new Date(Date.now() + ttlMs).toISOString(); const payload: SessionTokenPayload = { schema: "kxm.session-token.v1", sessionId: input?.sessionId ?? `session-${randomUUID()}`, issuedAt, expiresAt, ...(input?.agentName ? { agentName: input.agentName } : {}), ...(toolPolicy ? { toolPolicy } : {}), }; return Buffer.from(JSON.stringify(payload)).toString("base64url"); } export function isSessionTokenExpired(payloadOrToken: SessionTokenPayload | string): boolean { let payload: SessionTokenPayload | undefined; if (typeof payloadOrToken === "string") { try { const raw = Buffer.from(payloadOrToken.trim(), "base64url").toString("utf8"); payload = JSON.parse(raw) as SessionTokenPayload; } catch { return true; } } else { payload = payloadOrToken; } if (!payload || !payload.expiresAt) return false; const expiryTime = new Date(payload.expiresAt).getTime(); if (Number.isNaN(expiryTime)) return true; return Date.now() >= expiryTime; } export function parseSessionToken(token: string): SessionTokenPayload | undefined { try { const raw = Buffer.from(token.trim(), "base64url").toString("utf8"); const parsed = JSON.parse(raw); if ( parsed && typeof parsed === "object" && parsed.schema === "kxm.session-token.v1" && typeof parsed.sessionId === "string" ) { if (parsed.expiresAt) { const expiryTime = new Date(parsed.expiresAt).getTime(); if (!Number.isNaN(expiryTime) && Date.now() >= expiryTime) { return undefined; // Expired token fails closed } } return parsed as SessionTokenPayload; } } catch { return undefined; } return undefined; } function resolveUserConfigDirectory(overrideDir?: string | undefined): string { if (overrideDir) return resolve(overrideDir); return resolve(process.env.KXM_USER_CONFIG_DIR?.trim() || join(homedir(), ".config", "kxm")); } export function sessionTokenPath(userConfigDir?: string | undefined): string { return join(resolveUserConfigDirectory(userConfigDir), "session.token"); } export function persistSessionTokenToDisk( token: string, options?: { userConfigDir?: string | undefined; mode?: number | undefined } | undefined, ): string { const filePath = sessionTokenPath(options?.userConfigDir); const dir = dirname(filePath); mkdirSync(dir, { recursive: true, mode: 0o700 }); const mode = options?.mode ?? 0o600; writeFileSync(filePath, `${token.trim()}\n`, { encoding: "utf8", mode }); try { chmodSync(filePath, mode); } catch { // Ignore permissions error on filesystems that do not support POSIX modes } return filePath; } export function readSessionTokenFromDisk(options?: { userConfigDir?: string | undefined; } | undefined): { token: string; payload: SessionTokenPayload } | undefined { const filePath = sessionTokenPath(options?.userConfigDir); if (!existsSync(filePath)) return undefined; try { const token = readFileSync(filePath, "utf8").trim(); if (!token) return undefined; const payload = parseSessionToken(token); if (!payload) return undefined; return { token, payload }; } catch { return undefined; } } export function clearSessionTokenFromDisk(options?: { userConfigDir?: string | undefined; } | undefined): boolean { const filePath = sessionTokenPath(options?.userConfigDir); if (!existsSync(filePath)) return false; try { unlinkSync(filePath); return true; } catch { return false; } } function matchToolPattern(pattern: string, toolName: string): boolean { if (pattern === "*" || pattern === toolName) return true; if (pattern.endsWith("*")) { const prefix = pattern.slice(0, -1); return toolName.startsWith(prefix); } return false; } export function isToolAllowed(commandName: string, policy?: ToolPolicy): boolean { if (!policy) return true; const canonical = commandName.startsWith("kxm_") ? commandName : `kxm_${commandName}`; const bare = commandName.replace(/^kxm_/, ""); const denyList = policy.deny ?? policy.deniedTools; if (Array.isArray(denyList)) { for (const d of denyList) { if (d === canonical || d === commandName || d === bare || matchToolPattern(d, canonical)) { return false; } } } const allowList = policy.allow ?? policy.allowedTools; if (Array.isArray(allowList) && allowList.length > 0) { const matched = allowList.some( (a) => a === canonical || a === commandName || a === bare || a === "*" || matchToolPattern(a, canonical), ); if (!matched) return false; } if (policy.preset === "read-only") { const mutating = [ "kxm_send", "kxm_reply", "kxm_cancel", "kxm_fanout", "kxm_workflow_checkpoint", "kxm_workflow_record", "kxm_workflow_wait", "kxm_promote", ]; if (mutating.includes(canonical)) return false; } return true; } export function enforceToolPolicy( commandName: string, env: NodeJS.ProcessEnv = process.env, options?: { runId?: string; stageId?: string }, ): { allowed: boolean; error?: string; detail?: string } { const attemptTokenRaw = env.KXM_ATTEMPT_TOKEN?.trim(); if (attemptTokenRaw) { const attempt = parseAttemptToken(attemptTokenRaw); if (!attempt) { return { allowed: false, error: "attempt_token_invalid", detail: "KXM_ATTEMPT_TOKEN is malformed" }; } if (!isToolAllowed(commandName, attempt.toolPolicy)) { return { allowed: false, error: "tool_policy_denied", detail: `command ${commandName} is denied by attempt tool policy`, }; } // AttemptToken boundary: AttemptTokens are strictly worker tokens. // They cannot execute administrative state promotion unless explicitly allowed in toolPolicy if (commandName === "kxm_promote" || commandName === "promote") { const explicitAllow = attempt.toolPolicy?.allow ?? attempt.toolPolicy?.allowedTools; if (!Array.isArray(explicitAllow) || (!explicitAllow.includes("kxm_promote") && !explicitAllow.includes("promote") && !explicitAllow.includes("*"))) { return { allowed: false, error: "attempt_token_admin_denied", detail: "AttemptToken worker cannot perform operator state promotion without explicit policy grant", }; } } // AttemptToken boundary: Scoped strictly to runId if provided in execution options if (options?.runId && attempt.runId && options.runId !== attempt.runId) { return { allowed: false, error: "attempt_token_scope_violation", detail: `attempt token runId ${attempt.runId} does not match request runId ${options.runId}`, }; } return { allowed: true }; } const envSessionRaw = env.KXM_SESSION_TOKEN?.trim(); if (envSessionRaw) { const session = parseSessionToken(envSessionRaw); if (!session) { return { allowed: false, error: "session_token_invalid", detail: "KXM_SESSION_TOKEN is malformed or expired" }; } if (!isToolAllowed(commandName, session.toolPolicy)) { return { allowed: false, error: "tool_policy_denied", detail: `command ${commandName} is denied by session tool policy`, }; } return { allowed: true }; } const tokenFile = sessionTokenPath(env.KXM_USER_CONFIG_DIR); if (existsSync(tokenFile)) { let tokenRaw: string | undefined; try { tokenRaw = readFileSync(tokenFile, "utf8").trim(); } catch { return { allowed: false, error: "session_token_invalid", detail: "Session token file on disk could not be read" }; } const session = tokenRaw ? parseSessionToken(tokenRaw) : undefined; if (!session) { return { allowed: false, error: "session_token_invalid", detail: "Session token on disk is malformed or expired" }; } if (!isToolAllowed(commandName, session.toolPolicy)) { return { allowed: false, error: "tool_policy_denied", detail: `command ${commandName} is denied by session tool policy`, }; } return { allowed: true }; } return { allowed: true }; }