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

One Writer, Whoever Pressed

~14 min · single-writer, app-actions, audit-log, receipts, fastapi

Level 0Closed Lens
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Two Hands on One Lever

Lesson one promised that Pippa operates and Dad overrides through the same rules. Here is how the code keeps that promise. Take the most ordinary write there is, changing a setting. Pippa can do it through the set_setting app action. Dad can do it on Oriel's page. Follow both down and they land on the same two lines: engine.prefs.set(key, value, actor), which validates and stores, and engine.store.note(actor, "setting", ...), which writes the activity row. The doors differ in how they decide who is pressing. They do not differ in what pressing does.

The engine's docstring states the rule in its first lines: Pippa is the operator, Dad sees and overrides, and "both go through these methods, so a rule holds whoever pressed the button". There is no admin back door into the SQLite store, no page route that writes a table directly, no tool script that edits desired.json on its own. Each of those would be a second rule set waiting to drift.

Pippa's Door: the Family App-Action Contract

Pippa does not call Oriel's routes like a web client. Every family app declares what Pippa may do through one shared contract from the family kit, and cwkPippa's single app door reaches all of them the same way. The contract is a root class with rules no app can forget, because the kit enforces them before the app's handler runs:

  • R1, the caller. The door answers only a loopback caller that no proxy carried and no browser sent: no X-Forwarded-For, no Origin. A web page cannot reach it, even one served from the same Mac.
  • R2, the actor. The envelope names who acts (Pippa, which brain, which conversation, which surface), and the handler stamps that label on whatever it writes.
  • R3, draft by default. An action others could see runs as a draft unless the envelope carries publish and Dad's quoted instruction, which cwkPippa's door has already found in Dad's own recent message. An action with no draft form is refused without them. This is how the gate's open_eyes works later: it is that kind of action.
  • R4, receipts in the app. Every call that passes the HTTP caller check and reaches the action's own rules, refused ones included, is appended to a receipts file in Oriel's own store, never in Pippa's conversation. A read polled every few seconds may declare that it records only changes: a repeat with the same outcome for the same caller writes no new line, and the next line that differs says how many repeats it stands for.
  • R5, once. A create or send needs a request id; a replay answers the first result instead of writing again.
  • R6, small answers. Summaries and data are bounded, because the answer lands in a model's context; a list that can grow is paged instead of cut.

Dad's Door: the Page and His Session

The page is a React build the engine serves. Any page route that changes something or opens an eye calls require_dad first, which asks the family's PIN authority whether this browser holds Dad's live session (the details are a whole lesson in track six). If it does, the write is stamped dad. If it does not, the route refuses with the reason.

One class of page press needs no session at all: the stops. Closing every eye, parking the cameras, stopping play, stopping a recording, switching to Manual. A stop can only make Oriel do less, so it is never made to wait on a sign-in that might have expired at the wrong moment. Even signed out, the page shows those buttons. That asymmetry, loosening needs proof and tightening needs none, is the same one the eyes gate is built on.

What the Activity Log Buys

Because every write goes through the engine, every write leaves the same kind of row: a time, an actor, a kind and its details. The page shows the newest of them. When a camera opened, who opened it and with which words; when a setting changed and to what; when a gate refused and why. Pippa's calls also leave the kit's receipts, so a question like "did Pippa really ask for that?" has two independent answers.

This is the practical meaning of "Dad overrides". An override is not only the power to change a value; it is knowing what was changed, by whom, and being able to change it back through the same rules. A system where the operator's writes are invisible to the overseer is not overseen.

Code

Two doors, one method: Pippa's action and Dad's page route (trimmed excerpts, backend/actions.py and backend/app.py)·python
# backend/actions.py: Pippa's door, through the kit's app-action contract
@actions.action("set_setting", audience="private", effect="update",
                summary="Change one of Oriel's saved settings (spoiler guard, replay length, ...)")
def set_setting(args, ctx):
    try:
        value = engine.prefs.set(args["key"], args["value"], ctx.actor_label)
    except PreferenceError as exc:
        raise refuse(exc) from exc
    engine.store.note(ctx.actor_label, "setting", key=args["key"], value=value)
    return ActionResult(id=args["key"], summary=f"{args['key']} = {value}")


# backend/app.py: Dad's door, the page
@app.put("/api/settings/{key}")
def put_setting(key: str, body: SettingIn, request: Request):
    require_dad(request, "changing a setting")
    value = engine.prefs.set(key, body.value, "dad")
    engine.store.note("dad", "setting", key=key, value=value)
    return {"key": key, "value": value}
R1, the caller check every family app inherits (trimmed excerpt, the kit's kit_app_actions.py)·python
def trusted_caller(request) -> bool:
    """R1: loopback, no proxy hop, no browser."""
    host = getattr(getattr(request, "client", None), "host", "") or ""
    try:
        loopback = ipaddress.ip_address(host).is_loopback
    except ValueError:
        return False
    headers = getattr(request, "headers", {}) or {}
    return loopback and not headers.get("x-forwarded-for") and not headers.get("origin")
One activity table for every actor: a runnable sketch with SQLite·python
import json
import sqlite3
import time

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE activity (at REAL, actor TEXT, kind TEXT, detail TEXT)")


def note(actor, kind, **detail):
    with db:
        db.execute("INSERT INTO activity VALUES (?, ?, ?, ?)",
                   (time.time(), actor, kind, json.dumps(detail)))


note("pippa (claude, conversation 42)", "setting", key="replay_default_seconds", value=30)
note("dad", "setting", key="replay_default_seconds", value=20)
note("oriel", "gate_refused", input="look", why="no open approval")

for at, actor, kind, detail in db.execute("SELECT * FROM activity ORDER BY at DESC"):
    print(f"{actor:34} {kind:14} {detail}")

External links

Exercise

Run the SQLite sketch above, then extend it: add a set_setting(key, value, actor) function with a range check, make every write go through it, and add two callers (an "api" door and an "admin" door). Try to change a value out of range through each door and confirm both refuse with the same message and both leave an activity row.
Hint
If you are tempted to give the admin door a shortcut that skips the range check, write the shortcut as a separate, logged action with its own reason (Oriel's deploy override is one), never as a silent bypass.

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.