import type { EngineActionPolicy } from './actionPolicyEngine'; /** * Server-authoritative enforcement mode + policy version, pulled from * GET /api/cli/bundle and cached locally so the hot path (the Cursor hook) never * blocks on the network for the mode decision. * * Behavior: * - Fresh cache (< TTL): return immediately, no network. * - Stale cache: try a SHORT-timeout refresh; on success update + return; on * failure fall back to the last cached value (stale-while-error / fail-static). * - No cache + network fails: return source 'default' so the caller applies its * local fallback (the install-time flag). * * A console "Enforce" click flips shield.mode server-side; the machine picks it up * on the next refresh (no push to the machine required). */ export type ShieldMode = 'block' | 'monitor' | 'shadow'; export interface RuntimeBundle { mode: ShieldMode; version: string; policyHash?: string; policyCount?: number; pollIntervalMs?: number; /** Server-authoritative offline stance for this machine (hooks + gateway). */ failClosed?: boolean; /** Admin kill-switch: while true, hooks and the gateway deny ALL actions. */ suspended?: boolean; /** Admin-chosen extra posture scan folders for this machine (from the console). */ extraScanRoots?: string[]; /** Default scan folders the admin explicitly removed from this machine's posture sweep. */ disabledScanRoots?: string[]; /** Org auto-update policy: silently upgrade the CLI to targetVersion. */ autoUpdate?: { enabled: boolean; targetVersion?: string; }; /** Org honeypot policy: the daemon plants/removes decoy credential files. */ honeypot?: { enabled: boolean; }; /** * The org's enabled Action Policies (compact) — evaluated LOCALLY by hooks * and the MCP gateway when the backend is unreachable, with the exact same * engine the server runs (actionPolicyEngine.ts). The bundle `version` * covers policy edits, so console changes refresh this within one poll. */ actionPolicies?: EngineActionPolicy[]; /** * This machine's role profile (least-privilege preset assigned in the console). * Verb rules already ride `actionPolicies` as compiled policies; this block * carries the per-session AMOUNT limits enforced by local session counters * (hooks + MCP gateway). `null` limit = unlimited. */ machineRole?: { roleId: string; name: string; summary?: string; stage?: 'monitor' | 'enforce'; /** Split by target (db / file / overall); legacy pre-split keys are normalized on read. */ limits: import('./sessionLimits').SessionRoleLimits; }; /** One constrained, auditable action queued for the resident daemon. */ machineAction?: { id: string; type: 'health_check' | 'policy_refresh' | 'discovery_scan' | 'repair_protection' | 'upgrade_cli' | 'collect_diagnostics' | 'perf_snapshot'; reason: string; createdAt: string; expiresAt: string; /** Signed fields — present so the daemon can re-build the canonical payload. */ organizationId?: string; machineId?: string; /** Ed25519 control-plane signature; the daemon rejects unsigned/invalid actions. */ signature?: { signature: string; keyId: string; alg: string; version: string; }; }; } export interface FetchBundleInput { apiUrl: string; shieldId: string; shieldKey?: string; developerName?: string; machineName?: string; /** Exact fleet machine ID — makes the server's pending-action lookup deterministic. */ machineId?: string; /** Override cache TTL (ms). */ ttlMs?: number; /** Force a network refresh regardless of cache freshness. */ force?: boolean; /** * Fetch timeout override (ms). Default is the tight hot-path budget * (REFRESH_TIMEOUT_MS = 1.5s) — a hook must never stall a command on a slow * control plane. Diagnostic callers (doctor --health, deep self-test) pass a * generous value instead: a bundle answer at 3s is a healthy backend, not a * "no live bundle" failure. The 2h workday simulation surfaced this — ~1 in * 6 health checkpoints false-alarmed on p95 latency spikes. */ timeoutMs?: number; /** * Hot-path caller (hook / MCP gateway): when the cached mode is monitor or * shadow, accept a STALE cache without a synchronous refresh — the resident * daemon owns keeping the cache fresh (60s poll), so the developer never * pays the refresh timeout. Block-mode caches still refresh synchronously: * an enforce verdict must not run on arbitrarily old policy data. */ hotPath?: boolean; } export interface EffectiveBundle extends RuntimeBundle { source: 'server' | 'cache' | 'default'; /** Epoch ms of the last server 200/304 that confirmed this bundle (absent on the 'default' fallback). */ serverValidatedAt?: number; } /** * Resolve the effective runtime bundle for a Shield, using the local cache when * fresh and refreshing (briefly) when stale. Never throws — falls back to cache * or a 'default' marker so the caller can apply its local fallback. */ export declare function getRuntimeBundle(input: FetchBundleInput): Promise; export interface CredentialValidationResult { ok: boolean; /** true when the backend answered 401/403 — the key is WRONG, not offline. */ authRejected: boolean; detail: string; } /** * Prove the machine's credentials actually WORK against the backend — a * direct, cache-bypassing bundle fetch that exposes the HTTP outcome. * getRuntimeBundle() deliberately never fails (cache/default fallback keeps * enforcement alive offline), which is exactly wrong for onboarding/doctor: * "resolved a key locally" is not "the backend accepts it". The 8/12 incident * machine printed "Already enrolled ✓" while every hook call 401'd — * validation must separate rejected (re-enroll now) from unreachable (fine, * cached stance applies). */ export declare function validateShieldCredentials(input: { apiUrl: string; shieldId: string; shieldKey?: string; timeoutMs?: number; }): Promise; /** * Drop every cached runtime bundle at enrollment time. * * Why this exists: the cache file survives uninstall/reinstall and previous * enrollments. Without this reset, a machine that was once `block` inherits an * enforcing cache the moment it re-enrolls — and if the first bundle fetch is * blocked (corp proxy, partial install), it keeps ENFORCING under stale policy * on what the admin believes is a fresh monitor install. * * The cache is CLEARED, not re-seeded with the enrollment mode. Seeding looks * tempting (enrollment does return a mode) but is wrong: hot-path callers * accept a stale monitor/shadow entry WITHOUT refreshing (the daemon owns * freshness), so a seeded "monitor" would pin a machine whose real mode is * block into monitor until a daemon poll — unprotected, silently. With no * entry at all, the next hook/gateway call must ask the server, and the * existing offline contract covers the unreachable case. */ export declare function resetCacheOnEnrollment(): void; /** * Is ANY cached bundle marked suspended by an admin (kill-switch)? * * Deliberately dependency-free and never throws: this is called from the * hook's fail-open catch handlers, where the normal evaluation already died. * A suspended machine must stay locked down even when our own code crashes — * "exception = allow" must not become a kill-switch bypass. Reads only the * local cache file (no network, no config parsing). */ export declare function isSuspendedByCache(): boolean; /** * Admin scan-folder choices from the most recent cached bundle (any shield): * extra folders to add and default folders the admin removed. Read-only and * offline — used by `discover` so scheduled/daemon scans honor the console * selection without needing credentials plumbed in. */ export declare function getCachedScanRootOverrides(): { extraScanRoots: string[]; disabledScanRoots: string[]; }; /** @deprecated Use getCachedScanRootOverrides(). */ export declare function getCachedExtraScanRoots(): string[];