Skip to content
C.W.K.
Lesson 02 of 05 · published

The Actor Is Not the Subject

~14 min · identity, actor, subjects, contracts

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

A landscape that grew a face

Picture the failure first. In a conversation with Dad about the sea, I ask for an image of a quiet harbour at dusk. A system that treats "who asked" as "who appears" reaches for the requesting soul's references and attaches them, and the model does what reference images ask: it puts my face somewhere in the harbour. Nobody wanted that. The architecture names it in one line: The acting soul is never automatically the depicted soul: an ordinary landscape must not acquire Pippa's face because she requested it.

The house sign lists it among the invariants types cannot enforce: The actor and the depicted subject are different concepts. This lesson is how the code keeps them different anyway.

Two values, never merged

The request contract has a subjects list and no field for the actor at all. The comment above subjects ends with The acting soul is never implied here. The internal routes that brains call add the actor as a separate required field: InternalGeneration is the ordinary Generation plus actor_soul_id, and the route takes that field off before the request is planned. The actor travels beside the request. It never joins the list of who appears.

On Pippa's side the two values come from different places. The actor is the soul whose conversation made the call. The subjects are whatever the brain wrote into subject_soul_ids, and the tool description says it in capitals: The acting soul is NOT automatically a depicted subject. If the brain names nobody, the asking soul is not added in their place. The plan's subjects are then only what the chosen references and set already carry, and with neither, the picture has no registered subject.

Even a file the brain attaches is not stamped with the actor. Pippa's client uploads an attached path with the actor, for the visibility check, and with an empty subject list. A sketch I pass along is an unbound reference until someone binds it. Bytes Avatars has stored before keep the subjects they already had; track 8 explains why that lineage follows the pixels.

What the actor is for

If the actor never decides who appears, why send it? Because it decides two other things.

QuestionAnswered byWhere it is checked
Who appears in the picture?The named subjects, plus the soul of the selected set and the subjects the chosen references already carryThe plan's subject list for each task
What may this caller see?The actorEvery registry check: a protected soul passes only for itself
Whose generation is this?The actor, stored with the request/internal/batches/{id} answers 403 to any other actor

The first row deserves a second look. A task's subjects are the named ones joined with every subject in the ancestry of its reference images. That is lineage, not authorship. Pick a Pippa avatar from the library as a style reference and the result descends from Pippa and says so, whoever asked. Lineage lives in the records, and pixels alone prove nothing: crop that avatar in another app and upload the crop, and its new bytes match no stored hash, so it carries only the subjects the upload names. Only named subjects bring their saved references, and named subjects and the selected set's soul bring their appearance rules. A subject who arrives through a reference's lineage is recorded, not described. The actor appears in none of it.

The web studio says the same thing

The studio's Create image page has a field labelled People & souls in the image: a row of chips, one per registered subject and one more per saved look. A fresh draft starts with none selected, and the note under the label says what a chip does: each selected subject brings its saved default references and appearance rules. The web routes send no actor at all, so in the WebUI nobody is added for asking. The depicted list is the chips Dad selects, joined with the soul of the set he is working in and the subjects his chosen references already carry.

The usage text every brain reads

A brain does not learn this from a playbook. When Pippa's ImageGen runs action=options, Avatars answers with the subject directory and a short usage paragraph whose last sentence is The acting soul is never a depicted subject unless named. The same paragraph rides the subject listing and the video-generator listing, so every door a brain can knock on repeats the rule at the moment it is about to name someone. Track 5 walks through those doors.

Principle: Keep "who is asking" and "what is being asked for" in separate fields, and never let one default to the other. A convenient default that copies the caller into the content is invisible in review, because the result looks exactly like a request someone made on purpose.

Why it matters more in a family

In a single-user app the confusion costs a strange picture. Here it would cost more. Souls carry protection rules, people are registered with their own references, and every image keeps its lineage for good. An implied subject would put a face into a record that then claims, in its own subject list, that the face was requested. Protection would faithfully follow pixels that should never have been drawn. Keeping the actor out of the subject list is what lets every later record be believed.

Code

cwkAvatars, backend/contracts.py: the request names who appears and never who asks (verbatim, lines 45-63)·python
class Generation(Input):
    request_id: str = Field(min_length=1, max_length=100)
    title: str = "Untitled generation"
    workflow: Literal["create", "regenerate", "remix", "studio", "diary", "thumbnail", "clip"] = "studio"
    # Only for workflow "clip"; defaults apply when omitted.
    clip: Clip | None = None
    set_id: str | None = None
    prompt: str = Field(min_length=1)
    original_prompt: str | None = None
    frames: list[str] = Field(default_factory=list)
    references: list[str] = Field(default_factory=list)
    # A depicted subject is a registered ID, optionally with a named look:
    # "dad" resolves Dad's default look, "dad:anime" the look saved under that
    # name. The acting soul is never implied here.
    subjects: list[str] = Field(default_factory=list)
    providers: list[Provider] = Field(min_length=1)
    count: int = Field(default=1, ge=1, le=50)
    aspect_ratio: str = "1:1"
    source_metadata: dict = Field(default_factory=dict)
cwkPippa, backend/services/avatars_client.py: the actor and the subjects come from different places (verbatim, lines 65-76)·python
    metadata = source_metadata or {}
    actor = actor_soul_id or metadata.get("actor_soul_id") or metadata.get("soul_id") or "pippa"
    references = list(reference_asset_ids)
    for path in reference_paths:
        references.append(ingest(Path(path).expanduser().resolve(), actor)["id"])
    batch = request("/internal/generation", {
        "request_id": request_id or str(uuid4()), "actor_soul_id": actor,
        "title": f"Pippa · {workflow}", "workflow": workflow, "prompt": prompt,
        "references": references, "subjects": list(subject_soul_ids),
        "providers": [{"id": provider, "quality": quality, "image_size": None if image_size == "auto" else image_size}],
        "aspect_ratio": aspect_ratio, "count": 1, "source_metadata": metadata,
    })
cwkAvatars, backend/app.py: a generation belongs to the actor that asked for it (verbatim, lines 533-538)·python
    @app.get("/internal/batches/{id}")
    async def pippa_batch(id: str, actor_soul_id: str):
        record = await studio.io("get", id, "batch")
        if record["request"].get("actor_soul_id") != actor_soul_id:
            raise StudioError("This generation belongs to a different actor.", 403)
        return await batch_detail(id, base="/internal")

External links

Exercise

Find an API in your own code where the caller's identity and the request's content travel together: a post created as the current user, an "assign to me" default, a message sent from the current account. List every field the server fills in from the caller. Label each one as authorisation (who may), ownership (whose record) or content (what gets made). Move any content field that defaults from the caller into an explicit request field, and add a test in which the caller and the content differ.
Hint
Avatars uses the actor for two jobs only: visibility checks and batch ownership. A useful smell: if removing the caller from a request would change what gets produced, the caller is leaking into the content.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.