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": "yuki", "instance_id": "<uuid>", "created_at": "..."}``.
The name is generated once — a single female name from the Japanese,
American or Spanish pool (~150 in total; collisions across a swarm are
possible, rename by hand when they happen). 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

_JAPANESE = [
    "yuki", "sakura", "hana", "aki", "miku", "rin", "mei", "yui", "sana",
    "nao", "kaori", "mika", "rika", "emi", "haruka", "asuka", "rei", "airi",
    "kana", "honoka", "mitsuki", "natsumi", "sayuri", "suzume", "tamaki",
    "tomoe", "ayame", "chiyo", "fumiko", "himari", "kaede", "kanna", "mai",
    "mariko", "midori", "momoka", "natsuki", "noriko", "riko", "saori",
    "shiori", "suzuka", "tsukiko", "umeko", "yoshiko", "yuzuki", "kirie",
    "jun", "motoko", "satchiko",
]

_AMERICAN = [
    "emma", "olivia", "ava", "mia", "chloe", "grace", "lily", "hannah",
    "nora", "ruby", "alice", "clara", "daisy", "hazel", "iris", "jade",
    "june", "leah", "lucy", "pearl", "rose", "sage", "stella", "violet",
    "willa", "zoe", "amber", "brooke", "faith", "harmony", "josephine",
    "katherine", "lydia", "madeline", "natalie", "paige", "riley", "scarlett",
    "tessa", "trinity", "vivian", "abigail", "bethany", "delilah", "everly",
    "jenna", "leona", "melody", "serenity", "tiffany",
]

_SPANISH = [
    "sofia", "lucia", "valentina", "camila", "daniela", "carmen", "elena",
    "gabriela", "ines", "lola", "marisol", "paulina", "raquel", "rosalia",
    "serena", "teresa", "ximena", "alba", "bonita", "carlota", "esperanza",
    "juanita", "mirta", "noelia", "paloma", "rosita", "adela", "belen",
    "candela", "dolores", "estrella", "flor", "guadalupe", "jimena", "laura",
    "miriam", "pilar", "rocio", "veronica", "yolanda", "catalina", "dalia",
    "emilia", "fernanda", "gisela", "nayeli", "renata", "salma", "ursula",
    "vanesa",
]

_NAME_POOLS = [_JAPANESE, _AMERICAN, _SPANISH]  # union = the name pool

IDENTITY_FILE = "instance.json"


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


def generate_name() -> str:
    """A single female name from any culture pool, e.g. ``yuki`` or ``sofia``.

    Collisions across a swarm are possible (~150 names) — the owner renames
    by hand (edit instance.json) when two machines draw the same name.
    """
    import secrets

    return secrets.choice(_JAPANESE + _AMERICAN + _SPANISH)


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