refactor(realism): single source of truth for persona→login
decnet/realism/naming._home and decnet/canary/cultivator._persona_login both normalised "John Smith"→"johnsmith" with identical logic. Lift to decnet.realism.personas.login_for(persona) and have both consumers import it. Drift between the two would have left canary placement and realism path naming using different login derivations.
This commit is contained in:
@@ -29,6 +29,7 @@ from typing import Any, Optional
|
||||
from decnet.canary.base import CanaryArtifact, CanaryContext
|
||||
from decnet.canary.factory import get_generator
|
||||
from decnet.logging import get_logger
|
||||
from decnet.realism.personas import login_for
|
||||
from decnet.realism.taxonomy import ContentClass, Plan
|
||||
|
||||
log = get_logger("canary.cultivator")
|
||||
@@ -64,14 +65,6 @@ _DEFAULT_PATH: dict[ContentClass, str] = {
|
||||
}
|
||||
|
||||
|
||||
def _persona_login(persona: str) -> str:
|
||||
"""Mirror :func:`decnet.realism.naming._home`'s username conventions."""
|
||||
candidate = persona.lower().replace(" ", "")
|
||||
if candidate.isalnum() and candidate.isascii() and candidate:
|
||||
return candidate
|
||||
return "user"
|
||||
|
||||
|
||||
def _path_for(plan: Plan) -> str:
|
||||
"""Produce the canary placement path for *plan*.
|
||||
|
||||
@@ -84,7 +77,7 @@ def _path_for(plan: Plan) -> str:
|
||||
template = _DEFAULT_PATH.get(plan.content_class)
|
||||
if template is None:
|
||||
return plan.target_path
|
||||
return template.format(persona=_persona_login(plan.persona))
|
||||
return template.format(persona=login_for(plan.persona))
|
||||
|
||||
|
||||
def _new_callback_token() -> str:
|
||||
|
||||
@@ -24,6 +24,7 @@ import secrets
|
||||
import string
|
||||
from typing import Callable, Optional
|
||||
|
||||
from decnet.realism.personas import login_for
|
||||
from decnet.realism.taxonomy import ContentClass
|
||||
|
||||
|
||||
@@ -32,17 +33,8 @@ from decnet.realism.taxonomy import ContentClass
|
||||
# paths (out of scope until per-OS personas land). For now everything
|
||||
# is POSIX.
|
||||
def _home(persona: str) -> str:
|
||||
"""Return the canonical home directory for *persona*.
|
||||
|
||||
The persona's ``name`` is used as the linux username when it's a
|
||||
plausible login (lowercase, no spaces); otherwise we fall back to
|
||||
a generic ``user`` so the path doesn't reveal a persona display
|
||||
name on the decky filesystem.
|
||||
"""
|
||||
candidate = persona.lower().replace(" ", "")
|
||||
if candidate.isalnum() and candidate.isascii() and candidate:
|
||||
return f"/home/{candidate}"
|
||||
return "/home/user"
|
||||
"""Return the canonical home directory for *persona*."""
|
||||
return f"/home/{login_for(persona)}"
|
||||
|
||||
|
||||
def _random_token(rng: secrets.SystemRandom, length: int = 6) -> str:
|
||||
|
||||
@@ -117,6 +117,21 @@ def parse_personas(
|
||||
return out
|
||||
|
||||
|
||||
def login_for(persona: str) -> str:
|
||||
"""Return the linux login derived from a persona's display name.
|
||||
|
||||
Lowercase, strip spaces; if the result isn't a plausible POSIX
|
||||
login (alnum ASCII), fall back to ``user`` so the path doesn't
|
||||
leak the persona's display name onto the decky filesystem.
|
||||
Shared by realism path naming (``decnet/realism/naming.py``) and
|
||||
canary cultivation (``decnet/canary/cultivator.py``).
|
||||
"""
|
||||
candidate = persona.lower().replace(" ", "")
|
||||
if candidate.isalnum() and candidate.isascii() and candidate:
|
||||
return candidate
|
||||
return "user"
|
||||
|
||||
|
||||
def in_active_hours(persona: EmailPersona, now_hour: int) -> bool:
|
||||
"""Return True if *now_hour* (0–23) falls in the persona's window.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user