export interface InstanceMetadata { instance_id: string; created_at: string; hostname: string; daemon_version_first_seen: string; label: string | null; } /** * v2.4.0 Codex R3 MED — POSIX-safe shell single-quote escape for * printed-remediation strings. The lock function embeds the pidfile * path into an operator-facing `rm …` command in its error text. * Under `RELAY_HOME=/tmp/bad"$(touch OOPS)"`, a naive `rm "${pidFile}"` * would let a shell expand the command substitution if the operator * copy-pasted it. Escape strategy: * - Wrap the value in single quotes. * - Inside: replace every `'` with `'\''` (close quote, escaped * literal quote, reopen quote). This is the canonical POSIX-safe * approach and handles $(), backticks, $VAR, newlines, spaces. * * Exported for tests that pin this behavior (H1.3 hostile-path * regression). Keep in sync with any future error-message * remediation helpers. */ export declare function shellSingleQuoteEscape(value: string): string; /** * Returns true when the caller has explicitly opted into multi-instance * mode. Any of: * - `RELAY_INSTANCE_ID` env var set * - `~/.bot-relay/active-instance` symlink exists (set by `relay use-instance`) * - `~/.bot-relay/instances/` has ≥ 1 subdir * * Absent all three, we default to single-instance legacy mode. */ export declare function isMultiInstanceMode(): boolean; /** * Resolve the active instance id. Priority: * 1. `RELAY_INSTANCE_ID` env var (explicit per-invocation override) * 2. `~/.bot-relay/active-instance` symlink target (set by `relay use-instance`) * 3. null (single-instance legacy mode) */ export declare function resolveActiveInstanceId(): string | null; /** * Compute the per-instance directory. Returns null in single-instance * mode so callers can short-circuit to the legacy flat layout. */ export declare function instanceDir(instanceId: string | null): string | null; /** * Generate a fresh instance_id. UUID, not $USER. Caller persists it * via `createInstance`. */ export declare function generateInstanceId(): string; /** * Create a per-instance directory + write instance.json metadata. * Idempotent: re-creating an existing instance refreshes the * `daemon_version_first_seen` but leaves `created_at` intact. */ export declare function createInstance(instanceId: string, version: string, label?: string | null): InstanceMetadata; /** Read an instance's metadata. Returns null on missing / corrupt. */ export declare function readInstance(instanceId: string): InstanceMetadata | null; /** List all instances. Empty array in single-instance mode. */ export declare function listInstances(): InstanceMetadata[]; /** * Resolve the effective DB path for the active instance. Falls back * to the legacy `~/.bot-relay/relay.db` in single-instance mode. * `RELAY_DB_PATH` always wins if set (explicit operator override). */ export declare function resolveInstanceDbPath(): string; /** What this process actually resolved to, for assertion + announcement. */ export interface InstanceResolution { instanceId: string | null; dbPath: string; /** This MACHINE is set up for multi-instance (env, active link, or instances/ dirs). */ multiInstance: boolean; /** The dangerous state: machine is multi-instance, yet WE resolved to the flat legacy DB. */ legacyFallback: boolean; /** Set when RELAY_DB_PATH overrode everything — an explicit operator choice, never a fault. */ explicitDbPathOverride: boolean; } /** Describe — never throws. Safe for diagnostics (`relay doctor`, health output). */ export declare function describeInstanceResolution(): InstanceResolution; /** * REFUSE TO RUN MUTE — startup assertion. * * The injury this prevents is NOT "wrong path". A wrong path is loud: the * process fails to start and somebody notices. The injury is a QUIET REDIRECT * TO A DIFFERENT DATABASE — the process starts perfectly, registers, reports * healthy, and reads an empty mailbox forever, because it resolved to the flat * legacy `~/.bot-relay/relay.db` on a machine whose real data lives under * `~/.bot-relay/instances//`. Every symptom of that looks like "quiet * inbox". It cost this project nine days of invisible message loss. * * The check is the CONTRADICTION, not a missing env var: * isMultiInstanceMode() === true AND resolveActiveInstanceId() === null * i.e. this machine is demonstrably set up for instances, yet THIS process * found none. Keying on the contradiction is what makes the assertion safe for * legitimate single-instance users — they have no instances/ dir and no active * link, so `multiInstance` is false and this never fires for them. A blanket * "RELAY_INSTANCE_ID is required" would break every legacy and fresh install. * * Escape hatch: RELAY_ALLOW_LEGACY_FALLBACK=1 downgrades the refusal to a * shouted warning, for an operator who genuinely means to run against the flat * DB while instances exist. It warns rather than going silent, because silence * is the thing being fixed. */ export declare function assertInstanceResolution(emit: (msg: string) => void): InstanceResolution; /** * v2.4.0 Codex HIGH #2 patch — resolve the effective config path for * the active instance. Mirrors `resolveInstanceDbPath` exactly so DB * + config always live together (no split-brain where DB nests but * config stays flat). `RELAY_CONFIG_PATH` wins if set. */ export declare function resolveInstanceConfigPath(): string; /** * Acquire the per-instance lock. Writes a PID file at * `/instance.pid`. Returns a handle with a `release()` * callable; fail-closed when another daemon holds the lock for the * same instance_id. * * v2.4.0 Codex HIGH #1 patch (initial): atomic create-or-fail via * `openSync(..., 'wx')`. Closed the original "both daemons write" * race. * * v2.4.0 Codex HIGH #1 patch R2 (fail-closed, SECURITY hardening): * Codex re-audit reproduced a NEW TOCTOU in the R1 stale-PID reclaim * path: * 1. Initial pidfile contains PID 999999 (stale / dead). * 2. Process A: wx → EEXIST → read pid=999999 → probe ESRCH → about to unlink. * 3. Process A pauses (scheduler preemption) just before unlink. * 4. Process B: wx → EEXIST → read pid=999999 → probe ESRCH → unlink → wx succeeds → writes its live PID. * 5. Process A resumes → unlinks B's LIVE pidfile → wx succeeds → writes its own PID. * 6. Both A and B believe they hold the lock. Invariant violated. * * The auto-reclaim path cannot be made safe without an atomic "test * AND replace a specific prior content" primitive, which POSIX fs * doesn't provide. Deferred to v2.5+ with a proper primitive (fcntl * lock on the open fd, or a directory-based lock) + a regression * mirroring the exact Codex schedule. * * For v2.4.0: **fail-closed on every EEXIST**, regardless of PID * liveness. Operator manually removes the stale pidfile after * confirming no daemon is alive. "Slow UX, fast ship, provably safe." * Cross-platform. */ export declare function acquireInstanceLock(instanceId: string): { release: () => void; pidFile: string; }; /** * Set `~/.bot-relay/active-instance` to point at `instanceId`. Used by * `relay use-instance ` for kubectl-style context switching. * Overwrites any existing symlink. Validates that the instance * actually exists first. */ export declare function setActiveInstance(instanceId: string): void; //# sourceMappingURL=instance.d.ts.map