/** * kubectl-writer — push a ConfigMap to the customer's k8s cluster via * `kubectl apply`. * * Companion to the existing gitops/gh writer in configure-engine.ts. The * gh path opens a PR against the customer's gitops repo (slow loop: * review → merge → poll → engine hot-reload). This path is the * direct-apply alternative for environments where: * * - the engine pulls action-intent from a ConfigMap (new pull source * introduced alongside this writer), and * - the operator wants in-session iteration speed (~seconds vs * minutes), and * - the MCP has direct kubectl access (the user's local kubeconfig * pointed at the target cluster). * * Architecture: out-of-cluster. The MCP runs on the user's Mac and * invokes `kubectl apply -f - --dry-run=server`. The ConfigMap YAML is * built inline from caller-supplied content and piped via stdin. * * Safety model (layered): * 1. Caller flag (auto_apply=false / read_only=true on the tool) short- * circuits to dry-run; the rendered YAML is returned in-envelope. * 2. Even when auto-applying, a `--dry-run=server` pass ALWAYS runs * first to catch RBAC/quota/admission errors without writing. * 3. All user-controlled strings reach `kubectl` via process args * (spawnSync's argv array) — NEVER via shell interpolation. The YAML * body itself is piped through stdin, not constructed in argv. * 4. ConfigMap-size guard: k8s rejects objects > 1 MiB at the apiserver. * Pre-flight is on the SUM of value sizes, returning a structured * `request_entity_too_large` error before kubectl is invoked. * * Failure-classification: stderr from kubectl is regex-matched against * the common error families so the agent can branch on `error_type` * (forbidden, not_found, request_entity_too_large, conflict, timeout, * unknown). All errors flow back as `{ ok: false, error: {...} }` — * the function never throws. * * Why server-side dry-run by default: client-side dry-run only checks * schema; server-side runs the admission chain (RBAC, ResourceQuota, * webhooks). The latter is what the operator actually needs to know * about. A cluster with no server-side dry-run support falls back to * client-side, with a warning. */ import type { PrimitiveError } from '../primitive-errors.js'; /** * The k8s ConfigMap size limit is 1 MiB (apiserver enforcement). Allow * a small headroom so the metadata block doesn't push it over. */ export declare const CONFIGMAP_MAX_BYTES: number; export interface KubectlWriterArgs { /** Target k8s namespace. Must be DNS-1123-label valid. */ namespace: string; /** Target ConfigMap name. Must be DNS-1123-subdomain valid. */ configmap: string; /** * ConfigMap `data` map. Each value is a string; binary values are not * supported by this writer (the engine pulls action-intent.json which is * UTF-8 text). */ content: { [key: string]: string; }; /** * When true, skip the real apply and only run `--dry-run=server`. The * returned `dry_run_diff` carries kubectl's stdout describing what * WOULD change. When false, server-side dry-run still runs first as * a pre-flight, then do the real apply if the dry-run succeeded. */ dryRun: boolean; /** * Optional labels to merge into the ConfigMap metadata. Useful for * GitOps reconciliation markers (`app.kubernetes.io/managed-by=log10x`) * and for the engine's pull-source label selector. */ labels?: { [key: string]: string; }; /** * Optional annotations. Stamped with `log10x.com/written-at` so the * engine can log the freshness of the ConfigMap on each pull. */ annotations?: { [key: string]: string; }; /** Override the default timeout (ms). Tests pass a short value. */ timeoutMs?: number; /** * Override the kubectl binary path. Default `kubectl` (resolved via * PATH). Tests pass `/bin/false` etc. */ kubectlPath?: string; /** * Injected spawn implementation. Defaults to node's spawnSync. Tests * pass a mock that returns canned stdout/stderr/exit codes without * starting a real process. */ spawn?: SpawnSyncFn; } export interface KubectlWriterResult { ok: boolean; /** High-level state, branchable by the caller. */ status: 'applied' | 'dry_run_ok' | 'failed_validation' | 'failed_apply' | 'forbidden' | 'not_found' | 'request_entity_too_large' | 'conflict' | 'timeout' | 'kubectl_unavailable'; /** Server-side dry-run diff (kubectl stdout) — present on success. */ dry_run_diff?: string; /** Rendered ConfigMap YAML (always present so the caller can persist). */ rendered_yaml: string; /** Structured error envelope, present iff ok=false. */ error?: PrimitiveError; /** * Suggested kubectl one-liner the user can run to confirm the change * landed (`kubectl get configmap -n -o yaml`). */ verification_hint: string; } /** Trimmed surface of node's `spawnSync` return value. */ export interface SpawnSyncResult { status: number | null; stdout: string | Buffer; stderr: string | Buffer; error?: Error & { code?: string; }; signal?: NodeJS.Signals | null; } /** * Spawn function signature compatible with `child_process.spawnSync`. * Tests pass a mock that returns canned results. */ export type SpawnSyncFn = (command: string, args: string[], options: { input: string; timeout: number; encoding?: BufferEncoding; }) => SpawnSyncResult; /** * Apply a ConfigMap to the customer's cluster (or dry-run only) via * kubectl. Never throws — all failures flow back as `{ ok: false, ... }`. * * The function does: * 1. Validate the namespace + configmap names (DNS-1123). * 2. Pre-flight the size against the apiserver's 1 MiB cap. * 3. Render the ConfigMap YAML deterministically (sorted keys). * 4. Run `kubectl apply -f - --dry-run=server`. * 5. If dryRun=false and dry-run succeeded, run the real apply. * 6. Classify kubectl exit code + stderr into a structured error. */ export declare function applyViaKubectl(args: KubectlWriterArgs): KubectlWriterResult; interface RenderInput { namespace: string; configmap: string; content: { [key: string]: string; }; labels?: { [key: string]: string; }; annotations?: { [key: string]: string; }; } /** * Render the ConfigMap YAML. Public for testing — the format is stable * across kubectl versions. */ export declare function renderConfigMapYaml(input: RenderInput): string; /** * Map kubectl stderr text to a high-level status code. Exposed so the * top-level result can carry both the structured error and a coarse * branch the caller can switch on without parsing strings. */ export declare function classifyKubectlStderr(stderr: string): KubectlWriterResult['status']; /** * Build the kubectl one-liner the caller surfaces in `verification_hint` * so the user can confirm the apply landed. */ export declare function buildVerificationHint(namespace: string, configmap: string): string; export {};