/** Test-only (pattern: db.ts _resetAuditPurgeCounterForTests): lets the guard's * own negative controls exercise the FULL chokepoint against a temp "account * home" — a broken guard then writes a temp file, never the real config. */ export declare function _setAccountHomeForTests(p: string | null): void; export declare function assertNotRealUserConfigWrite(filePath: string): void; /** Parse a JSON file. Returns null on missing OR malformed (never throws) so a * hand-corrupted user file degrades to "treat as empty + back it up" rather * than crashing the installer. */ export declare function readJsonSafe(filePath: string): Record | null; /** * Atomically write `obj` as pretty JSON to `filePath`. Backs up any existing * file to `.bak` first (best-effort), writes to a temp sibling, then * renames over the target (atomic on POSIX). `mode` sets file perms. */ export declare function atomicWriteJson(filePath: string, obj: unknown, mode?: number): void; /** Structural deep-equality for JSON-ish values (order-insensitive on object * keys), used to make merges a true no-op when the target already matches. */ export declare function jsonEqual(a: unknown, b: unknown): boolean; export interface MergeResult { /** The merged root object (a NEW object; inputs are not mutated). */ root: Record; /** True if the merge changed anything (false → a no-op second run). */ changed: boolean; } /** * Reconcile the relay config: PRESERVE every existing key (operator edits + * `http_secret` + `instance_id` all win), and ADD any default key that is * missing. Never regenerates a secret, never overwrites a user value. A second * run with the same defaults is a no-op. * * (Shallow-by-top-level: the relay config is flat except `tool_visibility`, * which is preserved wholesale when present — we never reshape a user's block.) */ export declare function reconcileRelayConfig(existing: Record | null, defaults: Record): MergeResult; /** * Upsert an mcpServers entry by NAME. Preserves all other servers. Overwrites * OUR OWN named entry only when it structurally differs (so a path change * updates, but an identical re-run is a no-op). */ export declare function upsertMcpServer(root: Record | null, name: string, entry: Record): MergeResult; export interface SessionStartHookSpec { /** e.g. "startup|resume" */ matcher: string; /** absolute path invoked, e.g. "/abs/hooks/check-relay.sh" */ command: string; /** seconds */ timeout?: number; } /** * Upsert a SessionStart hook, deduped by SEMANTIC identity = the command path. * Preserves every other hook event AND every other SessionStart matcher-group * (unrelated hooks the operator already has). If a SessionStart entry already * invokes `command`, it is a no-op (no duplicate) — even if the matcher/timeout * were hand-tweaked, we do NOT clobber the operator's version. * * Claude Code settings hook shape: * { hooks: { SessionStart: [ { matcher, hooks: [ { type:"command", command, timeout } ] } ] } } */ export declare function upsertSessionStartHook(root: Record | null, spec: SessionStartHookSpec): MergeResult; /** * Is `command` the relay's SessionStart hook (`hooks/check-relay.sh`)? The * DETECTION / exact-match predicate — used where a false positive is expensive * (the installer's dedup; the tripwire's precise-watch set). NOT the only relay- * hook predicate; see "three predicates, three certainties" below. * * WHAT IT GUARANTEES — and what it does NOT (codex flagged three rounds of * over-claim on this one function; state the limit first). It is PRECISE FOR THE * FORMS WE EMIT: quoteForHookCommand only ever produces a single-quoted path, and * this owns exactly those + the legacy bare no-space path, and rejects the shapes * we never emit (`echo …`, unquoted `$()`/`;`, double-quoted, unquoted-whitespace). * It is NOT a general "is this really a relay invocation" oracle: the single-quote * branch owns ANY `'…/hooks/check-relay.sh'` BY SHAPE, so it DELIBERATELY * OVER-OWNS some foreign quoted paths — e.g. `'/foreign/hooks/check-relay.sh'` and * `'/bin/bash /x/hooks/check-relay.sh'` (which reads as one literal path token, not * a bash call). That over-ownership is WATCH-ONLY: its worst case is a false * tripwire ALARM (the accepted direction), never a destructive migration write * (migration uses exact-literal match, not this). We accept it because * disambiguating a single-quoted string's INTENT is undecidable and the cost is * only an alarm. * * THE RULE (two forms — init now emits only the first): * - SINGLE-QUOTED `'…'`: inside single quotes every byte is literal, so it is ONE * token — own by SHAPE, no metachar check: inner starts "/" and ends * "/hooks/check-relay.sh". This is quoteForHookCommand's canonical output, incl. * roots with `$`, `'`, backticks, `;` (all literal + safe inside `'…'`). * - OTHERWISE (unquoted / double-quoted): must be a BARE SAFE absolute path — * starts "/", ends the tail, NO shell metachar (|;&$<>`"CR LF), NO whitespace. * REJECTED: `echo /foreign/…`, `/bin/bash /foreign/…`, `/bin/bash foreign/…`, * wrong parent, .bak/dir suffix, bare basename, an UNQUOTED `$()`/`;` path * (metachar), a DOUBLE-quoted command (we never emit one — double quotes don't * stop expansion), an unquoted SPACED path (undecidable — see below). ACCEPTED: * init's single-quoted canonical (any root), a legacy raw NO-SPACE path, a * `%20`-fossil path. * * L4 AT THE HELPER LEVEL (ADR-0015 — codex #139 v4). quoteForHookCommand began as * a DISPLAY helper (quote only on whitespace). It was moved onto the install path, * where its output is EXECUTED as shell — a no-whitespace `$()`/apostrophe root * shipped RAW = command injection. A signal is authorization-grade only if its * CONTRACT says so; a quoting fn is security-grade only if its contract says it * handles shell metacharacters. Re-derive the contract when you move a helper onto * a new consequence — do not assume the name still fits. Fixed by ALWAYS * single-quoting; the classifier's single-quote branch reflects that canonical. * * WHY UNQUOTED-WITH-WHITESPACE IS NOT OWNED — the load-bearing decision. * `/a b/c` is EITHER the single path "/a b/c" OR the two tokens "/a" and "b/c", * and nothing in the string settles it without quoting or the filesystem. So a * real spaced install root (`/Users/x/Claude AI/…/check-relay.sh`) is * SYNTACTICALLY INDISTINGUISHABLE from an interpreter + relative script * (`/bin/bash foreign/…/check-relay.sh`). That is not a parser gap; it is a * property of the input. Standing rule: NO IRREVERSIBLE ACTION ON AN UNDECIDABLE * PREDICATE — quarantine it or make a caller assert it. So we DON'T own it here; * the operator makes it decidable by running `relay init`, which quotes it. * * THREE PREDICATES, THREE CERTAINTIES (ADR-0015, generalizes L2/L3 — the level of * certainty required scales with the CONSEQUENCE of a false positive): * - DETECTION / exact-match (this fn) → drives dedup + precise watch → must be * PRECISE, uniformly conservative on the undecidable class. * - WATCH (tripwire ambiguous-legacy marker) → drives only an ALARM → may be * BROADER; its worst case is a false alarm, so it may cover the undecidable * class — but its message must NOT overstate ownership (see the tripwire). * - MIGRATION (installHook) → drives a DESTRUCTIVE write → uses NO predicate at * all: EXACT LITERAL match against the string THIS install root would have * written. A heuristic authorizing a destructive write is the #128 defect. * * NOTE — installHook's migration does NOT call this predicate; it exact-matches * the literal string this root would have written. Detect and migrate ask * different questions with different failure costs; they are deliberately not one * shared predicate (the earlier "L4 single source" framing was withdrawn). */ export declare function isRelayCheckHookCommand(command: unknown): boolean; /** * Quote a hook-command PATH for embedding in a JSON "command" field. Claude Code * runs that string AS SHELL, and `relay init` writes it to the user's real * settings.json — so this is a SECURITY writer, not a display helper. * * ALWAYS SINGLE-QUOTE (codex #139 v4 P1). An earlier "quote only if it has * whitespace" was fit for emitting display text; on the shell-executed install * path it was a command-injection surface — a NO-whitespace root like * `/tmp/O'Hare/…` (unbalanced quote → broken hook) or `/x/$(id)/…` (bash runs * `id`) shipped RAW. Single-quoting is uniform and TOTAL: inside `'…'` every byte * is literal — `$ ` backtick `; & | > < * ? "` `\` space, apostrophes — nothing * expands. The only escape needed is the embedded single-quote, closed-reopened * as `'\''` (POSIX). No metacharacter blacklist — a blacklist is the wrong shape * for a quoting function; unconditional quoting is the defensible one. * * REFUSES a newline/CR-bearing path: no safe SINGLE-LINE shell command exists for * it (and both watch predicates reject control chars, so it would be unwatchable). * The one shared impl — `relay init` (installHook) and `relay generate-hooks` both * call it — so every emitted hook command is the canonical, precisely-ownable * single-quoted form. */ export declare function quoteForHookCommand(p: string): string; /** * Would `quoteForHookCommand(p)` succeed? The PREFLIGHT predicate — a single * source with quoteForHookCommand's throw condition, so init can validate BEFORE * it writes anything (a refusal must not be a partial commit — codex #139 v6 P1). * Only a newline/CR-bearing path is unquotable; every other byte single-quotes. */ export declare function canQuoteForHookCommand(p: string): boolean; /** * DETERMINISTIC migration of a SessionStart hook command — EXACT LITERAL match, * NO classifier, NO resemblance. If any SessionStart hook's command is EXACTLY * `rawCommand` (the unquoted string a prior `installHook` wrote for THIS install * root) and `rawCommand !== canonicalCommand` (i.e. the root has spaces and the * raw form needs quoting), rewrite exactly that string to `canonicalCommand`. * * WHY EXACT-MATCH AND NOT isRelayCheckHookCommand: this drives a DESTRUCTIVE write * (it replaces an entry). A heuristic authorizing a destructive write is the #128 * defect — a fuzzy predicate is fuzzy at the edges and the edges are where an * operator's own config lives. The installer KNOWS the exact string it would have * written, so it needs no predicate. Any shape it does not recognize byte-for-byte * (a different root, a `%20` path from elsewhere) is LEFT ALONE — the tripwire * surfaces those loudly; migrating them would be guessing. */ export declare function migrateRawHookCommand(root: Record | null, rawCommand: string, canonicalCommand: string): MergeResult; //# sourceMappingURL=config-merge.d.ts.map