/** * Phase 4b — per-repo agent cache at `/.cursell/agents/.json`. * * The cache JSON pairs the lockfile entry with the actual prompt * content. It is what the MCP `load_agent` handler reads at * runtime (Phase 4c) — the lockfile gives the trust contract * (version + hash) and the cache provides the bytes. * * Hash semantics: `sha256(content, utf8)` — only the `content` * field participates in the hash. Metadata (`name`, `description`, * etc.) is not part of the trust contract; it can be regenerated * from the registry without invalidating the lock entry. The * canonicalization is identical to Phase 2A's server-side * `computeContentHash` so client and server agree byte-for-byte. * * Metadata gate: before persisting the cache, the registry-supplied * `name` (≤100, no `\r`/`\n`) and `description` (≤280, no `\r`/`\n`) * are validated. The same predicate is enforced at read time by * Phase 4c (`list_pinned`), so a failure here surfaces a clear * `invalid_metadata` error pointing at the registry response — * not a confusing `cache_tampered` later. * * File mode 0o600 — only the user can read. */ import { z } from "zod"; import type { ContentHash } from "./schema.js"; /** Subdirectory under the repo where cache JSONs live. */ export declare const CACHE_SUBDIR = ".cursell/agents"; /** * The cache schema is a superset of the lockfile resolved info plus * the actual `content` bytes and derived metadata. The shape is * fixed by the plan (Phase 4b). */ export declare const CacheEntrySchema: z.ZodObject<{ slug: z.ZodString; version: z.ZodString; contentHash: z.ZodObject<{ algo: z.ZodLiteral<"sha256-v1">; value: z.ZodString; }, "strip", z.ZodTypeAny, { algo: "sha256-v1"; value: string; }, { algo: "sha256-v1"; value: string; }>; content: z.ZodString; name: z.ZodString; description: z.ZodString; sizeTag: z.ZodEnum<["lightweight", "medium", "large"]>; sizeWords: z.ZodNumber; category: z.ZodString; }, "strip", z.ZodTypeAny, { version: string; contentHash: { algo: "sha256-v1"; value: string; }; category: string; slug: string; name: string; description: string; sizeWords: number; sizeTag: "lightweight" | "medium" | "large"; content: string; }, { version: string; contentHash: { algo: "sha256-v1"; value: string; }; category: string; slug: string; name: string; description: string; sizeWords: number; sizeTag: "lightweight" | "medium" | "large"; content: string; }>; export type CacheEntry = z.infer; export declare class InvalidMetadataError extends Error { constructor(field: "name" | "description" | "category", reason: string); } export declare class CacheTamperedError extends Error { readonly slug: string; constructor(slug: string, reason: string); } /** * Validate the raw `name` / `description` / `category` per the * plan's cache-write metadata gate. The same predicate is reused * at read time in Phase 4c (`list_pinned` and `load_agent`). * * `category` is included because Phase 4c routes it through to * the activation banner AND the local statusline binary unwrapped; * a tampered cache file with `"category": "evil\n[FAKE STATUS]"` * would otherwise corrupt the statusline output even after the * hash check passed (category is hash-excluded). Length cap of 64 * matches the slug regex's overall character budget. */ export declare function assertValidMetadata(name: string, description: string, category?: string): void; /** * Canonical content hash — `sha256(content, "utf8")` lowercase hex. * Matches Phase 2A's server-side `computeContentHash` byte-for-byte. */ export declare function computeContentHash(content: string): ContentHash; export declare function cachePathFor(repoRoot: string, ref: string): string; /** * Read and shape-validate the cache entry at * `/.cursell/agents/.json`. Returns null if the * file is missing. Throws `CacheTamperedError` if the file exists * but fails schema validation. The caller is responsible for * verifying that `cache.slug === lockSlug` and `cache.version === * lock.resolved.version` before trusting the body. */ export declare function readCacheEntry(repoRoot: string, slug: string): Promise; /** * Write the cache entry atomically. Validates the metadata gate * before writing — failures raise `InvalidMetadataError` and the * file is not written. The write goes through a temp file (random * suffix + fsync + rename), so a crash mid-write leaves either * the previous bytes or the new bytes, never partial state. */ export declare function writeCacheEntry(repoRoot: string, entry: CacheEntry): Promise; /** Remove a cache entry. Idempotent — no-op if missing. */ export declare function deleteCacheEntry(repoRoot: string, slug: string): Promise;