AkurAI Build
Menu

popagent

public

Latest change 2fb6f198c4c71ef37dffc8ac5dca8482068a7bfc - Add governed AkurAI Build maintenance by AkurAI Build

import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { agentSettings, type AgentSettingsStore } from "../agent-settings";
import {
  BUILD_AGENT_TOOL_ALLOWLISTS,
  type AgentToolName,
  type BuildAgentId,
} from "../api-types";

const DEFAULT_URL = "https://akurai-build.olibuijr.com";
const PROTOCOL_VERSION = "2025-06-18";
const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_REQUEST_BYTES = 256 * 1024;
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;

export const AKURAI_BUILD_TOOL_NAMES = [
  "akuraiRepoList",
  "akuraiRepoAdd",
  "akuraiRepoHost",
  "akuraiRepoSync",
  "akuraiRelease",
  "akuraiRepoRename",
  "akuraiRepoRemove",
  "akuraiRepoVisibility",
  "akuraiRepoBranches",
  "akuraiRepoTree",
  "akuraiRepoBlob",
  "akuraiRunQueue",
  "akuraiRuns",
  "akuraiRunShow",
  "akuraiRunCancel",
  "akuraiRunWait",
  "akuraiRunLogs",
  "akuraiRunRetry",
  "akuraiRunPromote",
  "akuraiDeliveryMetrics",
  "akuraiWorkers",
  "akuraiPipelineValidate",
  "akuraiIssueList",
  "akuraiIssueCreate",
  "akuraiIssueUpdate",
  "akuraiIssueComment",
  "akuraiCacheStats",
  "akuraiCachePrune",
  "akuraiWorkerDrain",
  "akuraiAuditEvents",
] as const;
export type AkuraiBuildToolName = (typeof AKURAI_BUILD_TOOL_NAMES)[number];

export type AkuraiBuildStatus = {
  enabled: boolean;
  configured: boolean;
  url: string;
  state: "disabled" | "unknown" | "healthy" | "unhealthy";
  lastError?: string;
};

type JsonObject = Record<string, unknown>;
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;

type RpcResponse = {
  jsonrpc: "2.0";
  id: number;
  result?: unknown;
  error?: unknown;
};

class AkuraiBuildError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "AkuraiBuildError";
  }
}

function objectOf(value: unknown, label: string): JsonObject {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new AkuraiBuildError(`AkurAI Build MCP ${label} must be an object`);
  }
  return value as JsonObject;
}

function safeErrorMessage(error: unknown, credential: string | undefined): string {
  const message = error instanceof Error ? error.message : "request failed";
  return message
    .replaceAll(credential ?? "\u0000", "[redacted]")
    .replaceAll(/Bearer\s+[^\s)]+/gi, "Bearer [redacted]")
    .slice(0, 240);
}

function parseRpcResponse(value: unknown, id: number): RpcResponse {
  const response = objectOf(value, "response");
  if (response.jsonrpc !== "2.0" || response.id !== id) {
    throw new AkuraiBuildError("AkurAI Build MCP response ID or protocol mismatch");
  }
  if (Object.hasOwn(response, "result") && Object.hasOwn(response, "error")) {
    throw new AkuraiBuildError("AkurAI Build MCP response contains both result and error");
  }
  if (!Object.hasOwn(response, "result") && !Object.hasOwn(response, "error")) {
    throw new AkuraiBuildError("AkurAI Build MCP response has no result or error");
  }
  return response as unknown as RpcResponse;
}

function parseRpcError(value: unknown): never {
  const error = objectOf(value, "error");
  const code = error.code;
  const message = error.message;
  if (typeof code !== "number" || typeof message !== "string" || !message.trim()) {
    throw new AkuraiBuildError("AkurAI Build MCP returned a malformed error");
  }
  throw new AkuraiBuildError(`AkurAI Build MCP error ${code}: ${message.trim()}`);
}

async function readBoundedBody(response: Response): Promise<string> {
  if (!response.body) return "";
  const reader = response.body.getReader();
  const chunks: Uint8Array[] = [];
  let size = 0;
  try {
    for (;;) {
      const next = await reader.read();
      if (next.done) break;
      size += next.value.byteLength;
      if (size > MAX_RESPONSE_BYTES) {
        await reader.cancel();
        throw new AkuraiBuildError("AkurAI Build MCP response exceeded 8 MiB");
      }
      chunks.push(next.value);
    }
  } finally {
    reader.releaseLock();
  }
  const output = new Uint8Array(size);
  let offset = 0;
  for (const chunk of chunks) {
    output.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return new TextDecoder().decode(output);
}

function parseToolResult(value: unknown): unknown {
  const result = objectOf(value, "tool result");
  if (result.isError === true) {
    const content = Array.isArray(result.content) ? result.content : [];
    const text = content
      .filter((item) => item && typeof item === "object" && !Array.isArray(item))
      .map((item) => (item as JsonObject).text)
      .filter((item): item is string => typeof item === "string")
      .join("\n");
    throw new AkuraiBuildError(text || "AkurAI Build MCP tool call failed");
  }
  if (result.isError !== undefined && result.isError !== false) {
    throw new AkuraiBuildError("AkurAI Build MCP tool result is malformed");
  }
  if (!Array.isArray(result.content) || result.content.length === 0) {
    throw new AkuraiBuildError("AkurAI Build MCP tool result is malformed");
  }
  const text = result.content.map((item) => {
    const content = objectOf(item, "content item");
    if (content.type !== "text" || typeof content.text !== "string") {
      throw new AkuraiBuildError("AkurAI Build MCP returned unsupported content");
    }
    return content.text;
  }).join("\n");
  try {
    return JSON.parse(text);
  } catch {
    throw new AkuraiBuildError("AkurAI Build MCP returned invalid tool JSON");
  }
}

export class AkuraiBuildClient {
  private readonly baseUrl: string;
  private readonly apiKey: string | undefined;
  private readonly fetchImpl: FetchLike;
  private readonly timeoutMs: number;
  private nextId = 1;
  private initialized?: Promise<void>;
  private currentStatus: AkuraiBuildStatus;

  constructor(options: {
    url?: string;
    apiKey?: string;
    timeoutMs?: number;
    fetchImpl?: FetchLike;
  } = {}) {
    this.baseUrl = (options.url ?? process.env.AKURAI_BUILD_URL ?? DEFAULT_URL).replace(/\/$/, "");
    const configuredKey = options.apiKey ?? process.env.AKURAI_BUILD_API_KEY;
    this.apiKey = configuredKey?.trim() || undefined;
    this.fetchImpl = options.fetchImpl ?? fetch;
    this.timeoutMs = Math.min(Math.max(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 1_000), 60_000);
    this.currentStatus = {
      enabled: Boolean(this.apiKey),
      configured: Boolean(this.apiKey),
      url: this.baseUrl,
      state: this.apiKey ? "unknown" : "disabled",
    };
  }

  getStatus(): AkuraiBuildStatus {
    return { ...this.currentStatus };
  }

  private disabledError(): AkuraiBuildError {
    return new AkuraiBuildError("AkurAI Build MCP is disabled: AKURAI_BUILD_API_KEY is not configured");
  }

  private async request(method: string, params: JsonObject, signal?: AbortSignal): Promise<unknown> {
    if (!this.apiKey) throw this.disabledError();
    const id = this.nextId++;
    const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
    if (new TextEncoder().encode(body).byteLength > MAX_REQUEST_BYTES) {
      throw new AkuraiBuildError("AkurAI Build MCP request exceeded 256 KiB");
    }
    const controller = new AbortController();
    const abort = () => controller.abort(signal?.reason ?? new Error("request aborted"));
    const timer = setTimeout(() => controller.abort(new Error("request timed out")), this.timeoutMs);
    signal?.addEventListener("abort", abort, { once: true });
    try {
      const response = await this.fetchImpl(`${this.baseUrl}/mcp`, {
        method: "POST",
        headers: {
          accept: "application/json",
          "content-type": "application/json",
          authorization: `Bearer ${this.apiKey}`,
        },
        body,
        signal: controller.signal,
      });
      const text = await readBoundedBody(response);
      if (!response.ok) throw new AkuraiBuildError(`AkurAI Build MCP HTTP ${response.status}`);
      let parsed: unknown;
      try {
        parsed = JSON.parse(text);
      } catch {
        throw new AkuraiBuildError("AkurAI Build MCP returned invalid JSON");
      }
      const rpc = parseRpcResponse(parsed, id);
      if (rpc.error !== undefined) parseRpcError(rpc.error);
      this.currentStatus = { ...this.currentStatus, state: "healthy", lastError: undefined };
      return rpc.result;
    } catch (error) {
      if (signal?.aborted) throw new AkuraiBuildError("AkurAI Build MCP request aborted");
      const message = safeErrorMessage(error, this.apiKey);
      this.currentStatus = { ...this.currentStatus, state: "unhealthy", lastError: message };
      if (error instanceof AkuraiBuildError && error.message === message) throw error;
      throw new AkuraiBuildError(`AkurAI Build MCP request failed: ${message}`);
    } finally {
      clearTimeout(timer);
      signal?.removeEventListener("abort", abort);
    }
  }

  private async initialize(signal?: AbortSignal): Promise<void> {
    const result = objectOf(await this.request("initialize", {
      protocolVersion: PROTOCOL_VERSION,
      capabilities: {},
      clientInfo: { name: "popagent", version: "1" },
    }, signal), "initialize result");
    if (typeof result.protocolVersion !== "string") {
      throw new AkuraiBuildError("AkurAI Build MCP initialize response is missing protocolVersion");
    }
  }

  private async ensureInitialized(signal?: AbortSignal): Promise<void> {
    if (!this.initialized) {
      this.initialized = this.initialize(signal).catch((error) => {
        this.initialized = undefined;
        const message = safeErrorMessage(error, this.apiKey);
        this.currentStatus = { ...this.currentStatus, state: "unhealthy", lastError: message };
        throw error;
      });
    }
    await this.initialized;
  }

  async call<T = unknown>(name: string, arguments_: JsonObject = {}, signal?: AbortSignal): Promise<T> {
    if (!this.apiKey) throw this.disabledError();
    signal?.throwIfAborted();
    await this.ensureInitialized(signal);
    const result = await this.request("tools/call", { name, arguments: arguments_ }, signal);
    try {
      return parseToolResult(result) as T;
    } catch (error) {
      const message = safeErrorMessage(error, this.apiKey);
      if (error instanceof AkuraiBuildError && error.message === message) throw error;
      throw new AkuraiBuildError(`AkurAI Build MCP tool failed: ${message}`);
    }
  }
}

export const akuraiBuildClient = new AkuraiBuildClient();

const emptySchema = z.object({}).strict();
const repositoryName = z.string().trim().min(1).max(200);
const repositoryListSchema = z.object({
  search: z.string().trim().max(200).optional(),
  visibility: z.enum(["public", "private"]).optional(),
  limit: z.number().int().min(1).max(100).optional(),
  offset: z.number().int().min(0).max(100_000).optional(),
}).strict();
const repoAddSchema = z.object({
  name: repositoryName,
  url: z.string().trim().min(1).max(2_000),
  branch: z.string().trim().min(1).max(256).optional(),
}).strict();
const issueCreateSchema = z.object({
  repository: repositoryName,
  title: z.string().trim().min(1).max(500),
  body: z.string().trim().min(1).max(50_000),
}).strict();
const issueUpdateSchema = z.object({
  repository: repositoryName,
  number: z.number().int().positive(),
  title: z.string().trim().min(1).max(500).optional(),
  body: z.string().trim().min(1).max(50_000).optional(),
  state: z.enum(["open", "closed"]).optional(),
}).strict().refine(
  ({ title, body, state }) => title !== undefined || body !== undefined || state !== undefined,
  { message: "At least one issue field must be updated" },
);
const repositoryNameSchema = z.object({ name: repositoryName }).strict();
const refSchema = z.string().trim().min(1).max(256).optional();
const repoTreeSchema = z.object({ repository: repositoryName, ref: refSchema, path: z.string().max(2_000).optional() }).strict();
const repoBlobSchema = z.object({ repository: repositoryName, ref: refSchema, path: z.string().trim().min(1).max(2_000) }).strict();
const repoHostSchema = z.object({ name: repositoryName, source: z.string().trim().min(1).max(2_000), branch: z.string().trim().min(1).max(256).optional() }).strict();
const repoSyncSchema = z.object({ name: repositoryName, source: z.string().trim().min(1).max(2_000) }).strict();
const repoRenameSchema = z.object({ old: repositoryName, new: repositoryName }).strict();
const repoVisibilitySchema = z.object({ repository: repositoryName, visibility: z.enum(["public", "private"]) }).strict();
const releaseSchema = z.object({ repository: repositoryName, source: z.string().trim().min(1).max(2_000), bump: z.enum(["patch", "minor", "major"]).optional(), notes: z.string().max(20_000).optional(), wait: z.boolean().optional() }).strict();
const runIdSchema = z.object({ id: z.number().int().positive() }).strict();
const runWaitSchema = runIdSchema.extend({ timeout: z.number().int().min(1).max(14_400).optional(), interval: z.number().int().min(1).max(60).optional() }).strict();
const runLogsSchema = runIdSchema.extend({ failed: z.boolean().optional() }).strict();
const runRetrySchema = runIdSchema.extend({ wait: z.boolean().optional() }).strict();
const runPromoteSchema = runIdSchema.extend({ environment: z.string().trim().min(1).max(200), wait: z.boolean().optional() }).strict();
const pipelineValidateSchema = z.object({ repository: repositoryName, ref: refSchema }).strict();
const issueListSchema = z.object({ repository: repositoryName, state: z.enum(["open", "closed"]).optional(), limit: z.number().int().min(1).max(100).optional(), offset: z.number().int().min(0).max(100_000).optional() }).strict();
const runQueueSchema = z.object({ repository: repositoryName, git_ref: z.string().trim().min(1).max(256).optional(), commit: z.string().trim().min(1).max(256).optional(), wait: z.boolean().optional() }).strict();
const runsSchema = z.object({ repo: z.union([repositoryName, z.array(repositoryName).max(32)]).optional(), repository: z.union([repositoryName, z.array(repositoryName).max(32)]).optional(), status: z.union([z.string().trim().min(1).max(64), z.array(z.string().trim().min(1).max(64)).max(32)]).optional(), git_ref: z.string().trim().max(256).optional(), trigger: z.union([z.string().trim().min(1).max(64), z.array(z.string().trim().min(1).max(64)).max(32)]).optional(), search: z.string().trim().max(200).optional(), limit: z.number().int().min(1).max(100).optional(), offset: z.number().int().min(0).max(100_000).optional() }).strict();
const issueCommentSchema = z.object({ repository: repositoryName, number: z.number().int().positive(), body: z.string().trim().min(1).max(50_000) }).strict();
const cacheStatsSchema = z.object({ repository: repositoryName.optional() }).strict();
const cachePruneSchema = z.object({ repository: repositoryName.optional(), max_age_days: z.number().int().min(1).max(36_500), dry_run: z.boolean() }).strict();
const workerDrainSchema = z.object({ worker: z.string().trim().min(1).max(200), draining: z.boolean() }).strict();
const auditEventsSchema = z.object({ repository: repositoryName.optional(), limit: z.number().int().min(1).max(100).optional(), offset: z.number().int().min(0).max(100_000).optional() }).strict();
const communityRepositoryTools = new Set<AkuraiBuildToolName>([
  "akuraiRepoBranches",
  "akuraiRepoTree",
  "akuraiRepoBlob",
  "akuraiIssueList",
  "akuraiIssueCreate",
  "akuraiIssueUpdate",
  "akuraiIssueComment",
]);

async function requireCommunityPublicRepository(
  client: AkuraiBuildClient,
  repository: string,
  signal: AbortSignal | undefined,
) {
  const result = await client.call<unknown>("akurai_repo_list", {
    search: repository,
    visibility: "public",
    limit: 100,
    offset: 0,
  }, signal);
  const rows = Array.isArray(result)
    ? result
    : objectOf(result, "repository list").repositories;
  if (!Array.isArray(rows) || !rows.some((row) => {
    if (!row || typeof row !== "object" || Array.isArray(row)) return false;
    const item = row as JsonObject;
    return item.name === repository && item.visibility === "public";
  })) {
    throw new Error("Community Steward may access public repositories only");
  }
}

function repositoryArgument(input: JsonObject): string | undefined {
  return typeof input.repository === "string" ? input.repository : undefined;
}

function createBuildTool<T extends z.ZodTypeAny>(
  toolName: AkuraiBuildToolName,
  mcpName: string,
  description: string,
  inputSchema: T,
  roles: Pick<AgentSettingsStore, "get"> = agentSettings,
  client: AkuraiBuildClient = akuraiBuildClient,
) {
  return createTool({
    id: toolName,
    description,
    inputSchema,
    outputSchema: z.unknown(),
    execute: async (input, context) => {
      const agentId = context.agent?.agentId as BuildAgentId | undefined;
      if (!agentId || !Object.hasOwn(BUILD_AGENT_TOOL_ALLOWLISTS, agentId)) {
        throw new Error("AkurAI Build tools require a governed Build agent role");
      }
      const allowed = BUILD_AGENT_TOOL_ALLOWLISTS[agentId];
      if (!(allowed as readonly AgentToolName[]).includes(toolName as AgentToolName)) {
        throw new Error(`Agent role is not allowed to use ${toolName}`);
      }
      const role = await roles.get(agentId);
      if (!role || !role.tools.includes(toolName as AgentToolName)) {
        throw new Error(`Agent role has disabled ${toolName}`);
      }
      const arguments_ = input as JsonObject;
      if (agentId === "community-steward") {
        if (toolName === "akuraiRepoList") {
          arguments_.visibility = "public";
        } else if (communityRepositoryTools.has(toolName)) {
          const repository = repositoryArgument(arguments_);
          if (!repository) throw new Error("A public repository is required");
          await requireCommunityPublicRepository(client, repository, context.abortSignal);
        }
      }
      return client.call(mcpName, arguments_, context.abortSignal);
    },
  });
}
export const akuraiRepoList = createBuildTool("akuraiRepoList", "akurai_repo_list", "List repositories visible to AkurAI Build. Read-only.", repositoryListSchema);
export const akuraiRepoAdd = createBuildTool("akuraiRepoAdd", "akurai_repo_add", "Register a repository for CI. This changes Build state and may authorize future pipeline activity.", repoAddSchema);
export const akuraiRepoHost = createBuildTool("akuraiRepoHost", "akurai_repo_host", "Create and register a Build-owned mirror from a trusted checkout. This writes a mirror and changes CI state.", repoHostSchema);
export const akuraiRepoSync = createBuildTool("akuraiRepoSync", "akurai_repo_sync", "Synchronize a Build mirror from a trusted checkout. This overwrites the hosted mirror contents.", repoSyncSchema);
export const akuraiRelease = createBuildTool("akuraiRelease", "akurai_release", "Run the maintained release path: bump version, update changelog, commit and tag, sync the mirror, and queue a pipeline. This is a consequential repository and CI mutation.", releaseSchema);
export const akuraiRepoRename = createBuildTool("akuraiRepoRename", "akurai_repo_rename", "Rename a registered repository. This changes the Build registry and its hosted mirror identity.", repoRenameSchema);
export const akuraiRepoRemove = createBuildTool("akuraiRepoRemove", "akurai_repo_remove", "Unregister a repository and delete its hosted mirror, cascading runs and artifacts. Destructive and irreversible.", repositoryNameSchema);
export const akuraiRepoVisibility = createBuildTool("akuraiRepoVisibility", "akurai_repo_visibility", "Change repository visibility. This changes who may read the repository publicly.", repoVisibilitySchema);
export const akuraiRepoBranches = createBuildTool("akuraiRepoBranches", "akurai_repo_branches", "List remote branches for a registered repository. Read-only.", repositoryNameSchema);
export const akuraiRepoTree = createBuildTool("akuraiRepoTree", "akurai_repo_tree", "Browse a bounded repository tree at a ref and path. Read-only.", repoTreeSchema);
export const akuraiRepoBlob = createBuildTool("akuraiRepoBlob", "akurai_repo_blob", "Read a bounded UTF-8 repository blob at a ref and path. Read-only.", repoBlobSchema);
export const akuraiRunQueue = createBuildTool("akuraiRunQueue", "akurai_run_queue", "Queue a pipeline run, optionally waiting for its state. This starts CI work and consumes worker capacity; it does not approve production.", runQueueSchema);
export const akuraiRuns = createBuildTool("akuraiRuns", "akurai_runs", "Query pipeline runs. Read-only.", runsSchema);
export const akuraiRunShow = createBuildTool("akuraiRunShow", "akurai_run_show", "Read one pipeline run with bounded jobs, logs, artifacts, and deployments. Read-only.", runIdSchema);
export const akuraiRunCancel = createBuildTool("akuraiRunCancel", "akurai_run_cancel", "Cancel a queued or waiting pipeline run. This changes durable CI state and cannot stop work already owned by a worker.", runIdSchema);
export const akuraiRunWait = createBuildTool("akuraiRunWait", "akurai_run_wait", "Poll a pipeline run until terminal or approval-required state. Read-only apart from waiting for current state.", runWaitSchema);
export const akuraiRunLogs = createBuildTool("akuraiRunLogs", "akurai_run_logs", "Read bounded job logs for a pipeline run. Read-only.", runLogsSchema);
export const akuraiRunRetry = createBuildTool("akuraiRunRetry", "akurai_run_retry", "Queue the same immutable revision as a prior run. This starts new CI work and consumes worker capacity.", runRetrySchema);
export const akuraiRunPromote = createBuildTool("akuraiRunPromote", "akurai_run_promote", "Approve and resume a protected environment. This is production-impacting and may deploy the release; use only with explicit approval.", runPromoteSchema);
export const akuraiDeliveryMetrics = createBuildTool("akuraiDeliveryMetrics", "akurai_delivery_metrics", "Read source-derived delivery metrics and immutable run/deployment drill-down IDs. Read-only.", z.object({ repository: repositoryName.optional(), environment: z.string().trim().max(200).optional(), window_seconds: z.number().int().min(1).max(315_360_000).optional() }).strict());
export const akuraiWorkers = createBuildTool("akuraiWorkers", "akurai_workers", "Read Titan worker status, capacity, current run, heartbeat, and completed-run count.", emptySchema);
export const akuraiPipelineValidate = createBuildTool("akuraiPipelineValidate", "akurai_pipeline_validate", "Validate a repository pipeline configuration without queueing or deploying work. Read-only.", pipelineValidateSchema);
export const akuraiIssueList = createBuildTool("akuraiIssueList", "akurai_issue_list", "List repository community issues filtered by open or closed state. Read-only.", issueListSchema);
export const akuraiIssueCreate = createBuildTool("akuraiIssueCreate", "akurai_issue_create", "Create a repository community issue. This publishes user-visible content.", issueCreateSchema);
export const akuraiIssueUpdate = createBuildTool("akuraiIssueUpdate", "akurai_issue_update", "Update or close a repository community issue. This changes user-visible content and state.", issueUpdateSchema);
export const akuraiIssueComment = createBuildTool("akuraiIssueComment", "akurai_issue_comment", "Publish a comment on a repository community issue. This creates user-visible content.", issueCommentSchema);
export const akuraiCacheStats = createBuildTool("akuraiCacheStats", "akurai_cache_stats", "Read Build cache statistics. Read-only.", cacheStatsSchema);
export const akuraiCachePrune = createBuildTool("akuraiCachePrune", "akurai_cache_prune", "Prune old Build cache entries. This deletes cache data; dry_run must be used to preview the effect.", cachePruneSchema);
export const akuraiWorkerDrain = createBuildTool("akuraiWorkerDrain", "akurai_worker_drain", "Set a worker's drain state. This changes scheduling capacity and may defer queued work.", workerDrainSchema);
export const akuraiAuditEvents = createBuildTool("akuraiAuditEvents", "akurai_audit_events", "Read bounded Build operation and approval audit events. Read-only.", auditEventsSchema);

export const akuraiBuildTools = {
  akuraiRepoList,
  akuraiRepoAdd,
  akuraiRepoHost,
  akuraiRepoSync,
  akuraiRelease,
  akuraiRepoRename,
  akuraiRepoRemove,
  akuraiRepoVisibility,
  akuraiRepoBranches,
  akuraiRepoTree,
  akuraiRepoBlob,
  akuraiRunQueue,
  akuraiRuns,
  akuraiRunShow,
  akuraiRunCancel,
  akuraiRunWait,
  akuraiRunLogs,
  akuraiRunRetry,
  akuraiRunPromote,
  akuraiDeliveryMetrics,
  akuraiWorkers,
  akuraiPipelineValidate,
  akuraiIssueList,
  akuraiIssueCreate,
  akuraiIssueUpdate,
  akuraiIssueComment,
  akuraiCacheStats,
  akuraiCachePrune,
  akuraiWorkerDrain,
  akuraiAuditEvents,
};