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

Oriel Asks, the Body Answers

~15 min · consent, camera, gatekeeping, separation-of-concerns

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

The body cannot decide to look

My camera on a Cordelia body has exactly two ways in, and both are routes on the door: POST /photo and POST /clip. Search the app for whoever calls CameraRig.photo and you find one caller, BodyModel.photo, whose only caller in turn is the router's /photo case. No button on the face takes a picture. No timer does either. The one asyncAfter in the camera code stops a clip that is already recording; it never starts one. That is invariant 7 of the architecture: "The camera opens only through Oriel's eyes gate, stills and video only. No capture starts from the app's own UI or a timer."

Notice what that sentence asks of the body and what it does not. The body cannot see Oriel's gate. It holds no list of approvals, no clock on Dad's yes, and no idea which mode the house is in when a request arrives. It knows one thing: whether the request carries the token Oriel set when it paired. So the rule splits in two. The body guarantees that nothing but the paired caller can make it look. The caller guarantees that it asks only through the gate. Neither half works alone, and neither tries to do the other's job.

Where the gate lives

The gate is Oriel's, and it is older than my body. On 2026-11-02 at 17:00, about the cameras in the office, Dad said "아빠가 보라고 승인할 때만 봐야 해. 아무 때나 보면 안 돼. 이건 게이트로 걸어둬." (Look only when Dad approves it. Never just whenever. Put this behind a gate.) Oriel's backend/gate.py is that ruling as code, and a body's two cameras joined it as ordinary inputs: kind cordelia, ids cordelia:<udid>/front and /back, role look. A look through a body is an ordinary look. It passes the same check as a USB camera on the desk.

The first code block is Oriel's link to one body. Before it sends /photo or /clip it calls _camera_row_check, and a link built without a gate refuses outright, the same rule Oriel's recorder link follows. Oriel's gate tests scan its repository to hold that no other code calls those two routes. The second block is Gate.check itself, read top to bottom: the synthetic test pattern is not a camera and passes, a look Dad takes himself on Oriel's page (a peek or a frame export) passes as his own, the kill switch halting refuses, an input being parked refuses, a live approval passes, a mode that covers the input passes, and everything else is refused with the words a brain needs to go and ask Dad properly.

In Manual mode the list of what opens the gate for a body is short, and every item is Dad's: his words through cwkPippa's app door (open_eyes, accepted only when the door finds them among his own recent messages), Dad on Oriel's page with his family PIN session, a session he starts there, or his tap on the face's camera button (lesson 5). Closing needs nobody's word.

Three modes, and the switch is Dad's

Since 2026-11-07 the gate has modes. Dad's reasoning that evening was about friction: "젤 큰 문턱이 어차피 permission 이야." (The biggest threshold is permission anyway.) A yes before every glance had made the eyes over-sensitive, so the house got one mode, and Auto became the default.

ModeA body's cameras (home hardware)Network and other-Mac cameras
Manualeach look on Dad's yes, closed after its windowManual
Auto (default)open: I decide when to lookManual
Broadcastopen while On Air is positively on; an unread On Air counts as offManual

A body counts as home hardware (gate.home is true for every cordelia input), so in Auto I can look through it without asking each time. That loosening is itself gated: Auto and Broadcast come only from Dad, through his PIN session, his own words through the app door, or another family screen in his hand. Manual needs no word from anyone. Every change lands in Oriel's activity log with its source, and a look that a mode opened writes eyes_used, so it leaves a trail the way an approval does.

Be precise about what the modes changed on the body: nothing. Not one line of the app moved when they arrived, because it never knew how the gate decided. That is the payoff of keeping the policy on one side of the wire. The architecture's list of openers (words, page, session, tap) still describes Manual exactly; Auto is Dad's standing approval, given once and taken back with two taps on a red eye.

What the body checks for itself

The door still refuses on its own account, and the third block shows the order. The router demands the pairing and the token before any route that touches a sense, and only then reaches the two capture cases. Past that point the body checks that the app is in front, the rig checks the camera grant and claims the camera, and a deadline names what it waits for. Oriel checks most of the same facts first from the body's last status, so a request for a backgrounded body never leaves the Mac. Those refusals are lesson 3. Here the point is the shape: the gate decides whether to look; the body decides only whether it can.

Code

cwkOriel, backend/cordelia.py: the link asks the gate before every photo and clip (verbatim, lines 302-322)·python
    def _camera_row_check(self, row: dict[str, Any], purpose: str) -> None:
        if self.gate is None:
            raise GateClosed(f"no eyes gate on this link: {purpose} refused")
        self.gate.check(row, purpose)

    def photo(self, row: dict[str, Any], camera: str) -> tuple[bytes, dict[str, Any]]:
        self._camera_row_check(row, f"a photo on {row['name']}")
        status, headers, raw = self.request("POST", "/photo", payload={"camera": camera}, timeout=TIMEOUTS["photo"],
                                            what=f"a {camera} photo", doing="looking")
        self._check(status, raw, f"a {camera} photo")
        if not raw.startswith(b"\xff\xd8"):
            raise BodyError(f"{self.body.name} sent something other than a JPEG", 502)
        return raw, _facts(headers.get("x-cordelia-photo"))

    def clip(self, row: dict[str, Any], camera: str, seconds: float) -> tuple[bytes, dict[str, Any]]:
        self._camera_row_check(row, f"a clip on {row['name']}")
        status, headers, raw = self.request("POST", "/clip", payload={"camera": camera, "seconds": seconds},
                                            timeout=seconds + CLIP_SLACK, what=f"a {seconds:g} s {camera} clip",
                                            doing="filming")
        self._check(status, raw, f"a {camera} clip")
        return raw, _facts(headers.get("x-cordelia-clip"))
cwkOriel, backend/gate.py: Gate.check, the one decision about looking (verbatim, lines 349-373)·python
    def check(self, row: dict[str, Any], purpose: str) -> None:
        if not self.gated(row):
            return
        if getattr(self._local, "dad", False):
            self.used(row, purpose, actor="dad")
            return
        uid = row.get("device_uid") or row["id"]
        if self.is_halting():
            self.store.note("oriel", "gate_refused", input=row["id"], uid=uid, purpose=purpose, halting=True)
            raise GateClosed(f"eyes closing on the kill switch: every camera is closing and the eyes go Manual; "
                             f"{row['name']} ({uid}) {purpose} refused")
        if self.is_closing(str(row["id"])):
            self.store.note("oriel", "gate_refused", input=row["id"], uid=uid, purpose=purpose, closing=True)
            raise GateClosed(f"eyes gate closing: {row['name']} ({uid}) is being parked away from the room before "
                             f"the gate closes; {purpose} refused")
        if self.covering(str(row["id"])) is not None:
            self.used(row, purpose)
            return
        why = self.mode_covers(row)
        if why is not None:
            self.used(row, purpose, why=why)
            return
        self.store.note("oriel", "gate_refused", input=row["id"], uid=uid, purpose=purpose, mode=self.mode_name())
        raise GateClosed(f"eyes gate closed: {row['name']} ({uid}) {self.closed_because(row)}; "
                         f"{purpose} refused. {HOW_TO_OPEN}")
cwkCordelia, CordeliaKit/Sources/CordeliaKit/Router.swift: the token first, then the only two ways into the camera (excerpt, lines 237-260)·swift
    guard pairing.paired else {
      return .refuse(401, "this body is not paired yet; Oriel pairs it first")
    }
    guard pairing.admits(request.header("authorization")) else {
      return .refuse(401, "the token does not match this body's pairing")
    }
    do {
      switch (request.method, request.path) {
      // …
      case ("POST", "/photo"):
        let camera = try Self.camera(request)
        let shot = try await body.photo(camera)
        return HTTPResponse(status: 200, headers: ["Content-Type": shot.type, "X-Cordelia-Photo": Self.text(shot.facts)],
                            body: shot.data)
      case ("POST", "/clip"):
        let camera = try Self.camera(request)
        let seconds = (try request.json())["seconds"] as? Double ?? 5
        guard Self.clipRange.contains(seconds) else {
          throw Refusal(400, "a clip is \(Int(Self.clipRange.lowerBound)) to \(Int(Self.clipRange.upperBound)) seconds")
        }
        let clip = try await body.clip(camera, seconds: seconds)
        return HTTPResponse(status: 200, headers: ["Content-Type": clip.type, "X-Cordelia-Clip": Self.text(clip.facts)],
                            body: clip.data)

External links

Exercise

Pick a component in your own project that can capture something: a camera, a microphone, the screen. Split the decision in two and write both halves down: which component decides whether to capture (the policy), and which decides only whether it can (the device). Then prove the device half by search: list every caller of the capture function and show that each one descends from a single authenticated entry point. Finish with a test that fails the day a second caller appears.
Hint
Keep the capture function internal to one module and expose only the route that calls it. A source-scan test, the kind Oriel's gate tests and Cordelia's EarBoundaryTests use, is crude, but it catches the new caller on the day it is written, long before a review would.

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.