Newer
Older
navi-1 / navi / identity.py
"""Instance identity — a stable, human-friendly name for this Navi install.

Every deployed Navi carries ``instance.json`` in its working directory
(gitignored): ``{"name": "quiet-otter", "instance_id": "<uuid>", "created_at": "..."}``.
The name is generated once (adjective + animal — readable, speakable, collision-
free at household-network scale) and is meant to be renamed by hand if the owner
wants. The instance_id is the technical identity for audit and peer pinning.

Rename = edit the file, restart the server. Nothing else reads the name as
long-lived state, so this is safe.
"""

from __future__ import annotations

import json
import uuid as _uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

_ADJECTIVES = [
    "quiet", "amber", "hollow", "silver", "brisk", "cobalt", "dawn", "ember",
    "frost", "gentle", "harbor", "ivory", "jasper", "keen", "lucid", "mellow",
    "nimble", "opal", "pale", "quartz", "rapid", "soft", "tidal", "umber",
    "velvet", "wan", "crimson", "golden", "misty", "noble", "olive", "plum",
    "royal", "slate", "topaz", "vivid", "wild", "yellow", "zesty", "bold",
    "calm", "deep", "eager", "flint", "gray", "hasty", "iron", "jolly",
    "kind", "lunar",
]

_ANIMALS = [
    "otter", "falcon", "heron", "moth", "badger", "cormorant", "dingo", "elk",
    "fox", "grouse", "hare", "ibex", "jackal", "koi", "lynx", "marten",
    "newt", "osprey", "puffin", "quail", "raven", "stoat", "tapir", "urchin",
    "vole", "wren", "yak", "zephyr", "beagle", "caribou", "dolphin", "ermine",
    "ferret", "gecko", "haddock", "ibis", "jaguar", "kestrel", "lemur", "mink",
    "narwhal", "ocelot", "penguin", "quokka", "robin", "seal", "thrush",
    "umbra", "vicuna", "wallaby",
]

IDENTITY_FILE = "instance.json"


@dataclass(frozen=True)
class InstanceIdentity:
    name: str
    instance_id: str
    created_at: str


def generate_name() -> str:
    """Random adjective-animal pair, e.g. ``quiet-otter``."""
    import secrets

    return f"{secrets.choice(_ADJECTIVES)}-{secrets.choice(_ANIMALS)}"


def load_identity(path: str | Path = IDENTITY_FILE) -> InstanceIdentity:
    """Read instance.json. Raises if the file is missing or malformed."""
    data = json.loads(Path(path).read_text(encoding="utf-8"))
    return InstanceIdentity(
        name=str(data["name"]),
        instance_id=str(data["instance_id"]),
        created_at=str(data.get("created_at", "")),
    )


def ensure_identity(path: str | Path = IDENTITY_FILE) -> InstanceIdentity:
    """Load the identity, creating the file on first run (install / boot)."""
    p = Path(path)
    if p.exists():
        return load_identity(p)
    identity = InstanceIdentity(
        name=generate_name(),
        instance_id=str(_uuid.uuid4()),
        created_at=datetime.now(timezone.utc).isoformat(),
    )
    p.write_text(
        json.dumps(
            {
                "name": identity.name,
                "instance_id": identity.instance_id,
                "created_at": identity.created_at,
            },
            indent=2,
        )
        + "\n",
        encoding="utf-8",
    )
    try:
        p.chmod(0o644)
    except OSError:
        pass
    return identity