본문 바로가기
C.W.K.
Lesson 01 of 05 · published

소울과 사람, 와이어 필드 하나

~14 min · identity, registry, soul_pk, people

Level 0스케치
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

여기 누가 나와?

Avatars로 들어오는 이미지 요청은 결국 하나를 물어. 그림에 누가 나오냐는 거. Avatars는 이 답을 자기 기억에서 꺼내지 않아. 소울은 피파의 소울 레지스트리에 살고, 등록된 사람은 피파의 휴먼 레지스트리에 살아. Avatars는 매번 물어보고, 레지스트리랑 어긋날 수 있는 사본은 따로 안 들고 있어. WebUI의 소울 목록에도 한 줄로 적혀 있어. Soul identity comes from Pippa’s registry. 소울의 정체성은 피파 레지스트리에서 온다는 뜻이야. 옆 링크를 누르면 피파가 열려. 새 소울은 거기서만 등록하라는 거지.

이 트랙의 첫수가 이거야. 정체성은 주인이 하나인 데이터고, 나머지는 전부 그 주인한테 물어봐.

왜 키가 soul_pk야?

소울을 부르는 방법은 셋이야. soul_pk는 절대 안 바뀌어. 지금 슬러그랑 표시 이름은 바뀔 수 있는 겉모습이고. 피파의 리졸버는 불변 키, 현재 슬러그, 살아 있는 별칭 중 뭘 줘도 찾아내. 그래도 돌려주는 레코드엔 언제나 soul_pk가 들어 있고, Avatars가 저장하는 것도 그거야. 세트에도, 레퍼런스 프로필에도, 에셋의 피사체 목록에도. 하우스 사인의 불변식은 짧아. 레지스트리에 묶인 활성 소울만 아바타 세트를 가질 수 있고, 안정된 soul_pk는 이름이 바뀌어도 살아남는다. 다음 달에 소울 이름을 바꿔도 아바타, 레퍼런스, 혈통은 여전히 제 주인을 가리켜. 애초에 이름을 들고 있던 데가 없으니까.

소울이 어떻게 태어나고 등록되는지는 Pippa Quest에서 다뤄. 여기선 그림이 소울을 어떻게 찾아가는지만 봐.

사람도 피사체야

등록된 사람이 나오는 그림도 있어. 아빠의 일기 표지나 채널 썸네일엔 아빠가 자주 들어가. 근데 사람은 AI 소울이 아니야. 두뇌도, 볼트도, 목소리도 없어. 아키텍처 문서는 사람을 소울로 바꾸지 않고 같은 와이어 필드에 태우는 길을 골랐어. 피파 쪽 어댑터가 휴먼 레지스트리에서 HumanAvatarSubject를 만들어. 사용자 ID가 soul_pk에 들어가고, kind는 "human", is_super는 언제나 false야.

그래서 필드 하나에 둘 다 실려. 어느 레지스트리가 보증했는지는 kind가 말해줘. perpetual이랑 guest는 소울 레지스트리에서, human은 휴먼 레지스트리에서 와. 세트, 레퍼런스 프로필, 생성, 발행은 둘 다 똑같이 굴러가. dad도 pippa와 똑같이 그냥 피사체 ID야. 그래서 Avatars 안엔 출신 레지스트리를 가르는 코드가 필요 없어.

들어올 땐 엄격하게

리졸버는 소울 레지스트리를 먼저 보고, 없으면 휴먼 레지스트리를 봐. 두 번째 조회가 엄격한 쪽이야. 피파의 사용자 레지스트리엔 조회 함수가 일부러 둘 있어. 너그러운 쪽은 모르는 ID가 오면 아빠라고 답해. 뷰어가 안 적힌 옛 로그 한 줄엔 그게 맞아. 엄격한 쪽은 예외를 던지고. 아바타 경로는 엄격한 쪽을 써. 그 줄에 붙은 주석 a typo must never become Dad가 이유 전부야. 오타가 아빠가 되는 일은 절대 없어야 한다는 거지. 철자 틀린 소울 ID가 조용히 아빠로 풀리면 그림에 엉뚱한 얼굴이 들어가. 뒤따르는 기록도 전부 그게 의도였다고 말할 거고.

이어서 validate_subjects가 피사체마다 두 가지를 더 봐. 하나, 활성 상태여야 해. 레지스트리에서 물러난 소울은 새 작업에 못 나와. 둘, 부탁한 소울이 적혀 있으면, 보호된 Super Soul은 자기 자신이 부탁했을 때만 통과해. 호출한 쪽이 어떤 종류의 거절인지 알 수 있게, 피파 라우트는 PermissionError를 403으로, ValueError를 409로 바꿔. 보호는 트랙 8에서 제대로 다뤄. 여기선 이 검사가 Avatars가 아니라 레지스트리가 사는 곳에서 돈다는 것만 잡고 가.

뷰어 모드는 사람이 아니야

피파 사용자 레지스트리엔 사람만 있는 게 아니야. 예를 들어 콰이어트 모드는 아빠 세션을 게스트 뷰로 바꿔서 screen_safe 표시가 붙은 대화를 숨겨. 이런 접근용 투영이 몇 개 더 있어. 다 사용자 레지스트리의 한 줄이라서, 대충 투영하면 전부 아바타 피사체가 돼버려. 베이스 클래스가 그 문을 닫아. avatar_root는 실제 사람이 직접 켜지 않는 한 None이야. human_subject는 그게 없는 사용자를 This user is a viewer context, not an avatar subject로 거절해. 뷰어 문맥이지 아바타 피사체가 아니라는 말이야. 피사체 목록도 같은 식으로, 아바타 루트가 있는 사용자만 모아서 만들어. 아키텍처 문서는 이걸 한 문장으로 적어 뒀어. 모르는 ID와 뷰어 모드는 거절한다.

원칙: 권한은 이름이 아니라 레코드에 둬. 와이어 필드 하나에 두 종류의 정체성을 실어도 안전한 조건은 둘이야. 레코드마다 어느 레지스트리가 보증했는지 적혀 있을 것, 그리고 그 필드를 받는 조회가 모르는 ID를 짐작하지 않고 거절할 것.

Avatars는 매번 물어봐

Avatars의 Upstream.subjects는 ID 목록에 부탁한 소울을 곁들여 피파의 /api/avatar-studio/subjects로 보내. Studio.subjects는 묻기 전에 중복을 걸러내고. 계획을 세울 때, 레퍼런스 프로필을 읽거나 저장할 때, 에셋을 레퍼런스로 열 때, 두뇌한테 보여줄 피사체 디렉터리를 그릴 때 매번 물어. 레지스트리에서 멀어질 로컬 사본이 없어. 비용은 피파한테 거는 호출 한 번이야. 그 대신 오늘 아침 레지스트리에서 물러난 소울은, Avatars를 아무도 안 고쳤는데도 오후 계획부터 안 나와.

어디에도 안 묶인 사진도 받아. 레퍼런스가 전부 등록된 피사체를 그리는 건 아니거든. 빛이 좋아서 올린 풍경 사진은 피사체 목록이 빈 평범한 구도 레퍼런스야. 그걸 소울인 척 꾸밀 필요도 없고.

Code

cwkPippa, backend/services/avatar_studio.py: 소울 와이어 필드에 실린 등록된 사람 (원문 그대로, 34-43행)·python
@dataclass(frozen=True)
class HumanAvatarSubject:
    # soul_pk remains the existing wire key; kind distinguishes its authority.
    soul_pk: str
    current_slug: str
    display_name: str
    avatar_root: str
    kind: str = "human"
    status: str = "active"
    is_super: bool = False
cwkPippa, backend/services/avatar_studio.py: 소울 먼저, 사람은 엄격하게, 피사체는 하나하나 검사 (원문 그대로, 54-81행)·python
def human_subject(user):
    """Project the canonical human registry without creating an AI soul."""
    if not user.avatar_root:
        raise ValueError("This user is a viewer context, not an avatar subject")
    return HumanAvatarSubject(user.user_id, user.user_id, user.display_name, user.avatar_root)


def resolve_avatar_subject(id: str):
    try:
        return resolve_soul_entry(id)
    except UnknownSoulError:
        return human_subject(require_user(id))  # strict: a typo must never become Dad


def list_avatar_subjects():
    return [*list_soul_entries(include_guest=True),
            *(human_subject(user) for user in list_users() if user.avatar_root)]


def validate_subjects(ids: list[str], actor: str | None = None) -> list[dict]:
    entries = [resolve_avatar_subject(value) for value in ids]
    actor_entry = resolve_soul_entry(actor) if actor else None
    for entry in entries:
        if entry.status != "active":
            raise ValueError("The soul is no longer active in the registry")
        if actor_entry and entry.is_super and actor_entry.soul_pk != entry.soul_pk:
            raise PermissionError("This soul is not visible to the acting soul")
    return [soul_record(entry) for entry in entries]
cwkPippa, backend/users/base.py: 실제 사람이 켜지 않으면 뷰어 모드엔 아바타 루트가 없어 (원문 그대로, 116-126행)·python
class UserAdapter(ABC):
    """One human viewer of cwkPippa. ``DadUser`` is canonical."""

    @property
    def avatar_root(self) -> str | None:
        """Published avatar copies for an actual person, not a viewer mode.

        Opt in explicitly; GuestView and other access projections are not
        new people or image-generation subjects.
        """
        return None

External links

Exercise

작업 중인 시스템에서 두 종류의 존재가 ID 필드 하나를 같이 쓰는 곳을 찾아봐. 사용자와 서비스 계정, 사람과 봇, 고객과 조직 같은 거. 지금은 읽는 쪽이 둘을 어떻게 구별하는지, 각 종류를 어느 레지스트리가 보증하는지, 모르는 ID가 오면 조회가 뭘 하는지 적어. 레코드를 쓰거나 사람한테 보여주는 자리에서 조회가 기본값으로 폴백한다면, 그 경로가 예외를 던지게 바꾸고 한 글자 틀린 ID를 거절하는 테스트를 붙여.
Hint
종류를 말해주는 필드부터 찾아. Avatars에선 kind가 perpetual, guest, human 중 하나야. 그런 필드가 없으면 ID 모양이나 조회 순서로 종류를 짐작하고 있다는 뜻이고, 바로 그 짐작에서 오타가 엉뚱한 레코드가 돼.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.