import type { Meta } from '../types.js';
import type { IgnoredDeviceEntry } from '../fleet/types.js';
/** Operating-system family of a device, used to pick the remote shell. */
export type DevicePlatform = 'windows' | 'linux' | 'macos' | 'unknown';
/** Remote shell dialect derived from the platform. */
export type DeviceShell = 'powershell' | 'posix';
/** How `agents ssh` authenticates to a device. Both are first-class, fully
 * non-interactive: `key` uses the ssh agent / on-disk keys, `password` pulls
 * the secret from a Keychain-backed secrets bundle via an askpass shim. */
export type DeviceAuthMethod = 'key' | 'password';
/** How to reach a device on the network. */
export interface DeviceAddress {
    /** Where the address came from: a Tailscale node, or a manual entry. */
    via: 'tailscale' | 'manual';
    /** Fully-qualified DNS name (Tailscale MagicDNS), without a trailing dot. */
    dnsName?: string;
    /** Raw IP address (IPv4 preferred). */
    ip?: string;
}
/** Authentication settings for a device. */
export interface DeviceAuth {
    method: DeviceAuthMethod;
    /** Explicit private-key path passed to OpenSSH for key authentication. */
    identityFile?: string;
    /** Secrets bundle holding the password (when method === 'password'). */
    bundle?: string;
    /** Key within the bundle whose value is the password. Defaults to 'password'. */
    bundleKey?: string;
}
/** Last-known Tailscale reachability snapshot for a device. */
export interface DeviceTailscale {
    online: boolean;
    /** True when the last handshake was a direct (non-relayed) connection. */
    direct: boolean;
    /** DERP relay region code (e.g. 'sfo'); empty when direct. */
    relay?: string;
    lastSeen?: string;
}
/**
 * Verdict of the last live SSH reachability probe (RUSH-1965).
 *
 * The live `agents devices` probe already learns whether a box answered right
 * now; this persists that answer so the online/offline word is read from a
 * fresh probe instead of the potentially-stale {@link DeviceTailscale.online}
 * snapshot (which only `agents devices sync` / the daemon ever writes). A
 * `via:"manual"` device — which never gets a tailscale peer entry — gets a
 * reachability verdict this way too, so it stops rendering "offline forever".
 */
export interface DeviceReachability {
    /** Whether the last live probe reached the device. */
    reachable: boolean;
    /** Transport the verdict came through — the address kind used to dial it. */
    via?: DeviceAddress['via'];
    /** ISO-8601 timestamp of the probe that produced this verdict. */
    checkedAt: string;
}
/** A single registered device. */
export interface DeviceProfile {
    name: string;
    platform: DevicePlatform;
    shell: DeviceShell;
    user?: string;
    address: DeviceAddress;
    auth: DeviceAuth;
    tailscale?: DeviceTailscale;
    /** Last live SSH-probe reachability verdict (RUSH-1965). Preferred over the
     * cached {@link DeviceTailscale.online} snapshot when rendering online/offline,
     * because the live probe reflects whether the box answered right now. */
    reachability?: DeviceReachability;
    createdAt: string;
    updatedAt: string;
}
/**
 * Whether a fan-out should dial this device, honouring the preference stated on
 * {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
 * {@link DeviceTailscale.online} snapshot.
 *
 * Reading only `tailscale.online` is wrong in both directions, and both were
 * live on a real fleet. A `via:"manual"` device never gets a tailscale peer
 * entry at all, so its `online` is permanently `undefined` and a strict
 * `=== true` test skipped it forever — every session on that box was invisible
 * to the cross-fleet sweep. Conversely a box that has since gone to sleep keeps
 * a stale `online:true` and gets dialed, burning a full ConnectTimeout and
 * reporting a false "unreachable" that callers treat as doubt.
 */
export declare function isDialableDevice(d: DeviceProfile): boolean;
/** Map of device name to profile. */
export type DeviceRegistry = Record<string, DeviceProfile>;
/**
 * `--device` values that mean "resolve me", not "connect to a box with this
 * name". A real device registered under one of these would be unreachable,
 * and pinning `interactive.host` to one is a misconfiguration that has to fail
 * at WRITE time — otherwise the read side can only report "none is set", which
 * tells the user to run the command they just ran.
 */
export declare const RESERVED_DEVICE_NAMES: Set<string>;
/**
 * Throw if `name` is not usable as an ssh alias (no spaces, quotes, etc.).
 *
 * SHAPE ONLY. Safe on read paths, which must keep working for a name that is
 * already registered — including one this version would refuse to create.
 */
export declare function assertValidDeviceName(name: string): void;
/**
 * Throw if `name` cannot be used for a NEW device: bad shape, or a reserved
 * routing sentinel.
 *
 * Deliberately separate from {@link assertValidDeviceName}, and called from
 * exactly two kinds of place: `agents devices add <name>`, and the config keys
 * that point AT a device (`interactive.host`).
 *
 * Not from `upsertDevice`, `addIgnored` or the discovery writers. Those all
 * receive tailnet node names the user never typed — `devices sync` upserts every
 * observed node in a loop with no per-node catch, so one node named `auto` would
 * abort the entire sync and register nothing after it. A name the fleet OBSERVES
 * is not a name anyone CHOSE; only the second kind is policy's business.
 */
export declare function assertRegistrableDeviceName(name: string): void;
/** Map a Tailscale `OS` field to our platform enum. */
export declare function platformFromOs(os: string | undefined): DevicePlatform;
/** The remote shell a platform speaks. */
export declare function shellForPlatform(platform: DevicePlatform): DeviceShell;
/**
 * Load all devices from the registry file. Returns an empty object only when
 * the file does not exist. A malformed file is a hard error — silently
 * returning {} would let the next write wipe the user's device list.
 */
export declare function loadDevices(): Promise<DeviceRegistry>;
/**
 * Synchronous {@link loadDevices} for callers already on a sync path (e.g. the
 * `agents sessions --device` fan-out builds its ssh command strings synchronously
 * and only needs the target's platform to pick POSIX vs PowerShell). Same
 * missing-file/corruption contract as {@link loadDevices}.
 */
export declare function loadDevicesSync(): DeviceRegistry;
/** Get a single device profile, or null if it is not registered. */
export declare function getDevice(name: string): Promise<DeviceProfile | null>;
/** Fields a caller may supply when creating or updating a device. */
export interface DeviceInput {
    platform?: DevicePlatform;
    user?: string;
    address?: DeviceAddress;
    auth?: DeviceAuth;
    tailscale?: DeviceTailscale;
    reachability?: DeviceReachability;
}
/**
 * Create the device if absent, otherwise merge the supplied fields into the
 * existing profile. `shell` is always re-derived from the (possibly new)
 * platform so the two can never drift. Returns the resulting profile.
 */
export declare function upsertDevice(name: string, input: DeviceInput): Promise<DeviceProfile>;
/**
 * Persist live reachability verdicts for many devices in one locked pass
 * (RUSH-1965). A no-op verdict (same `reachable` and not newer than what's
 * stored) is skipped, so a cache-served `agents devices` render doesn't churn
 * the registry every call; only a flip or a fresher probe writes. Unlike
 * {@link upsertDevice} this does NOT bump `updatedAt` — reachability is
 * transient liveness (like the tailscale snapshot), not a profile edit. Unknown
 * device names are ignored. Returns the names actually updated.
 */
export declare function writeReachability(updates: Record<string, DeviceReachability>): Promise<string[]>;
/** Remove a device. Returns false if it was not registered. */
export declare function removeDevice(name: string): Promise<boolean>;
/**
 * The ignore-list: tailscale node names the user explicitly dismissed from
 * auto-discovery. A dismissed node is deliberately NOT a device (it never
 * enters the registry), so it has no per-device doc — its home is the central,
 * TRACKED `~/.agents/agents.yaml` under a `fleet.ignored` list, which syncs
 * fleet-wide via `agents repo push/pull`: a dismissal on one box stops the
 * suggestion on every box (RUSH-3062). Auto-discovery (`runDeviceSync`'s
 * pending diff) subtracts this set, so an ignored node never re-surfaces as a
 * suggestion. Writes go through `updateMeta` — the central-config write path
 * (withMetaLock + atomic write) — so two agents writing concurrently cannot
 * corrupt it, and no second lock is hand-rolled here.
 */
export type { IgnoredDeviceEntry } from '../fleet/types.js';
/**
 * The EFFECTIVE ignore-list: the union of every box's device doc
 * `fleet.ignored` (deduped by node name, newest `ignoredAt` winning) plus any
 * lingering central-legacy `fleet.ignored` block (drained by the migration).
 * Deterministic and order-independent. A malformed block is a hard error for the
 * same reason the registry is: silently returning [] would let the next write
 * wipe the user's dismissals.
 */
export declare function loadIgnoredEntries(meta?: Meta): IgnoredDeviceEntry[];
/** Load the set of ignored node names. Same corruption contract as
 * {@link loadIgnoredEntries}. */
export declare function loadIgnored(): Promise<Set<string>>;
/** True if `name` is on the ignore-list. */
export declare function isIgnored(name: string): Promise<boolean>;
/**
 * Union `names` into the meta's ignore-list, stamping new entries with
 * `ignoredAt` and THIS machine's id. Existing entries keep their original
 * who/when, so re-adding a name is a true no-op. Returns the input unchanged
 * when no name is new. Exported for the one-shot legacy-store migration in
 * lib/devices/config-migration.ts.
 */
export declare function withIgnoredAdded(meta: Meta, names: string[], ignoredAt: string): Meta;
/** Add a node name to THIS box's ignore-list (device doc). Idempotent. Returns
 * the resulting cross-box union of dismissed names. Reads only the device docs
 * for the return value — a corrupt central-legacy block surfaces loudly on the
 * effective read path ({@link loadIgnoredEntries}), never blocks a per-box
 * write that does not touch central at all. */
export declare function addIgnored(name: string): Promise<Set<string>>;
/** Remove a node name from the ignore-list (un-ignore). Returns false if it was
 * not ignored. */
export declare function removeIgnored(name: string): Promise<boolean>;
