The last door before the public internet
Everything so far guards the studio from the inside. The public site is a different boundary. Its images live on Cloudflare Images, and an uploader script on a Mac pushes them there when a site commit carries new pictures. A protected face must never make that trip, whatever the file is called and wherever in the repository it sits.
The architecture names the rule and its trap. Super Soul originals and anything derived from them cannot be uploaded to Cloudflare Images. The exporter revalidates current policy and recorded lineage immediately before upload. Hiding something in a UI is not enough, and an exporter must consume Avatars' decision rather than infer safety from a directory. The site's policy module opens with the reason, in two lines: Avatars is the lineage authority, and the upload boundary checks bytes, so a renamed protected avatar or a derived cover cannot slip past a folder deny list.
Two answers from Avatars
Studio.export_policy(id) answers for one asset. It resolves the asset's lineage, asks Pippa about every subject with no actor attached, and lists the Super ones as blocked_subjects. The answer is allowed only when that list is empty. Its comment adds the rule from Track 2: an archived copy of a protected image is still a protected image, so the archive flag is never read here.
The hash door answers for bytes. It finds every asset whose SHA-256 matches, runs export_policy on each, and folds the results: known is whether anything matched, allowed is whether every match is allowed, and the blocked subjects are the union. The same handler answers on two paths: /internal/export-policy for a local engine, and /api/export-policy behind the admin gates of the next lesson.
What "known" means
Follow the code when nothing matches. matches is an empty list, policies is empty, and Python's all() over an empty list is True. Bytes Avatars has never stored come back as known: false, allowed: true. The architecture's phrase that missing policy blocks public export can be read as "unknown bytes are blocked". The code does not do that, and it could not honestly do anything else: Avatars can refuse bytes it knows descend from a protected soul, and it cannot vouch for bytes it never saw. Most of the site's images are screenshots and diagrams that never passed through the studio.
What fails closed is the answer, not the file. The site's gate throws, and nothing is uploaded, in every case except a well-formed allow.
| Situation | Door says | Site gate |
|---|---|---|
| Bytes with a protected lineage | known: true, allowed: false | throws |
| Bytes Avatars stored, all public | known: true, allowed: true | passes |
| Bytes Avatars never stored | known: false, allowed: true | passes |
| Door address cannot be resolved | nothing | throws |
| No answer within 15 seconds | nothing | throws |
| Any non-2xx status | a refusal | throws |
allowed not exactly true, or known not a boolean | a malformed body | throws |
That last row is quiet but important. A body of {"allowed": true} with no known is refused, so a proxy or a stub that does not speak the contract cannot pass for the real door. The site's test feeds the gate a protected answer, that malformed answer, a 503 and a thrown network error, and requires all four to reject. The gate returns known to its caller, and today neither caller acts on it.
Ask before the bytes change
The check is keyed by the original bytes, so it has to run before anything alters them. putToCfImages calls the gate first, before it strips EXIF and GPS for publication and before it deletes or creates any public image. The asset sync does the same for an oversized file: it checks the original before the resize changes its content hash. Lesson 2 explains why the order matters. A stripped or resized copy no longer matches the original's hash, so unless those exact bytes were stored too, it is a stranger to Avatars, and a stranger answers allowed.
Fail closed, twice, for real
The gate shipped on 2026-09-29, and it has failed closed in production twice. Until a fix the next day, 2026-09-30, every new image upload came back 403. Avatars answers /internal/ only to a loopback caller with no x-forwarded-for, origin or sec-fetch-mode header, and Node's built-in fetch adds sec-fetch-mode: cors to every request. The fix was a plain node:http GET, with a test that pins the headers it does not send.
The second time came from the other direction. Avatars had moved to the Pippa machine in late October, and the gate still defaulted to its own Mac's loopback port, where nothing answered. Every site commit with a new image stopped. The fix, on 2026-10-25, asks the family registry where Avatars runs: its loopback when it runs here, otherwise an ssh tunnel to the serving Mac's loopback. The door stays loopback-only, and a new test fails if the door resolves to this Mac's own port while Avatars runs elsewhere.
Both outages were loud, and neither let a picture through. That is the whole trade of failing closed: a broken check looks like a stopped commit, never like a quiet pass.