/** * Phase 4c — lockfile-pinned `load_agent` and `list_pinned` helpers. * * The MCP server consults `cursell.lock` (walking up from the project * directory pinned at startup) on every `load_agent` call. When a * lockfile is found: * - slug present in `lock.agents` → STRICT serve from * `/.cursell/agents/.json`, with a hash recompute on * every load. Drift = `cache_tampered`. Mismatched * slug/version = `cache_tampered`. * - slug NOT in `lock.agents` → STRICT error pointing at * `cursell add `. * * When no lockfile is found, this module returns null and the * caller falls back to the existing PERMISSIVE Hub-fetch flow. * * `list_pinned` returns a structured array of (slug, name, version, * description, sizeTag) per lockfile entry. The author-controlled * `name` and `description` are first validated against the same * predicate `cursell add` enforces at write time (CR/LF + length * caps) and then wrapped with a per-call nonce framing — without * the framing, a tampered cached `description` could remap the * picker to a different valid-looking pin. */ import type { Lockfile, AgentEntry } from "../lockfile/schema.js"; import { type CacheEntry } from "../lockfile/cache.js"; export declare class CacheMissingError extends Error { readonly slug: string; readonly cachePath: string; constructor(slug: string, cachePath: string); } export declare class NotPinnedError extends Error { readonly slug: string; readonly lockfilePath: string; constructor(slug: string, lockfilePath: string); } export declare class LockfileUnreadableError extends Error { readonly lockfilePath: string; readonly code: string; constructor(lockfilePath: string, code: string); } export interface ResolvedLockfile { /** Path to the lockfile that was found. */ lockfilePath: string; /** Repo root — the directory containing the lockfile. */ repoRoot: string; /** Parsed lockfile body (already invariant-checked by readWithFingerprint). */ lock: Lockfile; } /** * Walk up from `projectDir` looking for `cursell.lock`. Returns the * resolved lockfile + repo root, or null if no lockfile is found. */ export declare function resolveLockfile(projectDir: string): Promise; export interface PinnedLoad { /** Lockfile entry (spec + resolved.version + contentHash + ...). */ entry: AgentEntry; /** Cache file body (slug, version, content, name, description, sizeTag, ...). */ cache: CacheEntry; /** Path of the cache file (for error messages). */ cachePath: string; } /** * Strict load: read the cache, verify slug/version/hash against * the lockfile entry. Throws on any mismatch. * * The hash recompute runs on every call — trust must be re-asserted * each time the agent is served, not just at install. A tampered * cache file (text changed without updating the hash) is detected * here. */ export declare function loadPinnedAgent(resolved: ResolvedLockfile, slug: string): Promise; export interface PinnedListEntry { /** * Phase 3 of agent-namespace: canonical `/` ref — * the trusted lockfile key. Renamed from v0 `slug` so the picker's * selection key and the AI's `load_agent({ref})` argument are the * same identifier emitted from the same source. */ ref: string; /** Version from the LOCKFILE entry (trusted), not the cache. */ version: string; /** Cleaned + framed display name (from the cache). */ name: string; /** Cleaned + framed display description (from the cache). */ description: string; /** Size tag from the cache (server-domain enum, validated). */ sizeTag: "lightweight" | "medium" | "large"; /** Per-row error if the cache for this slug failed to load. */ error?: string; } /** * Read every `lock.agents` entry's cache, validate metadata gate * + slug/version cross-check, and return a structured array. The * caller is responsible for wrapping `name` / `description` with * the per-call nonce framing — see `mcp/index.ts list_pinned` * handler. (We do the validation but NOT the cleaning/framing * here so this module stays free of presentation concerns.) */ export declare function listPinnedAgents(resolved: ResolvedLockfile): Promise;