Essay

The Same Shape, at Home

7 min read

I once argued, at length and repeatedly, that authoring a policy and enforcing it are different jobs, and that a platform trying to do both competes with everyone and is trusted by no one. We took the authoring layer and refused the rest.

That was a choice. At home it isn't.

Every enforcement point for my family's screen rules belongs to somebody else. Apple owns Screen Time and prohibits one app from reading another's restrictions. Family Link has no public API. Roblox, YouTube and Xbox are account settings and nothing more. I could not build the enforcement layer here if I wanted to, and nobody is going to hand me one.

So the shape I spent years defending at work turns out to be the only shape available at home. You get the authoring layer, or you get nothing.

The problem nobody names

Open any of these apps and you will find a settings page that is entirely honest. Roblox will tell you truthfully what Roblox does. Screen Time will tell you truthfully what the iPad does. Each one shows you a green tick for its own slice.

The trouble is what a green tick reads as. It reads as protected. It means protected here, and it is silent — not wrong, silent — about the four other routes to the same thing. A rule like "no screens after half nine" is one sentence to a parent and five unrelated settings pages to a computer, and no page anywhere shows you all five at once.

Then there is drift. An OS update resets a toggle. A kid finds the browser instead of the app. Somebody changes a setting to get through one evening and never changes it back. You set all this up in June and it is now September and you genuinely do not know what is still true.

Nothing in this stack lies to you. It just never shows you the whole board, and never tells you when a square has changed.

Define once

So I built the small version of the thing I refused to build big. It's called Policy Plane, and it does four things.

You write the rule once, in your own words, with typed parameters — Alex may use Roblox for at most 90 minutes a day. The English on screen is generated from the parameters rather than typed alongside them, so the sentence cannot drift from the thing being checked.

It works out which surfaces can carry that rule, and which cannot. A surface has to reach that child, handle that kind of rule, and govern that app. Anything else is not a gap you should feel bad about; it's a fact about the vendor.

It translates the rule into each vendor's own click-path. One sentence becomes Screen Time's Downtime and Roblox's Screen Time and Xbox's Screen time — three different procedures from one thing you wrote.

And where a vendor can be read, it reads it. A browser extension opens the settings page in a tab you can watch, using the session you are already signed into, and reports what the page actually said.

One rule routed to every surface that can carry it A single rule — Alex may use Roblox for at most ninety minutes a day — fans out to six surfaces. Screen Time, Roblox and Xbox can carry it; only Roblox can be read back automatically. NextDNS, Family Link and YouTube are out of scope, each for a stated reason. Alex may use Roblox for at most 90 minutes a day written once Screen Time can carry it a human confirms Roblox can carry it read back automatically Xbox can carry it a human confirms NextDNS out of scope — no time rules at all Family Link doesn't cover Alex YouTube doesn't govern Roblox A dashed box is not a gap you should feel bad about. It is a fact about the vendor, and the page says which one.
One sentence, six surfaces, three different answers about whether it can even be expressed.

The extension reads. The plane judges.

That last part is the piece I'd defend hardest, and it is one line of design.

The extension returns the text the page showed — the literal string "1 Hour". It never returns a verdict. A separate process re-parses that string and decides what it means.

A component that both reads a setting and rules on whether that setting is correct is grading its own homework. And the failure is silent, because a reader that misparses will usually misparse in the direction that makes it look successful. Splitting them means the only thing that can be wrong is a string, and a wrong string produces unknown rather than a confident lie.

The trust boundary between the plane and the extension The dashboard sends only a recipe identifier and parameters to the extension. The extension reads the vendor page using only wait, click and read, and returns only the raw text it saw, never a verdict. A separate judge decides what the text means. THE PLANE dashboard the judge THE EXTENSION service worker waitFor · click · read VENDOR PAGE your logged-in session recipe id + params never selectors, steps or URLs read only raw text readings, and a result code never a verdict Two crossings, each narrow on purpose.
The extension knows what a recipe means. The page only knows what to ask for.

Two other constraints fell out of the same instinct. The engine's entire action set is waitFor, click, read — there is no verb for writing, so a recipe cannot change a setting because there is no way to express it. And the page sends the extension a recipe id and parameters, never selectors or steps or URLs, so a mistaken page cannot aim a logged-in browser session at somewhere it chose.

Five verdicts, and the two that earn their place

A check comes back as satisfied, stricter, not satisfied, unexpressible, or unknown. Only the first two count as in force.

Unexpressible means the vendor does not offer that value at all. One of the vendors I test against offers 30, 60 and 120 minutes and nothing between. Ninety minutes cannot be set there — not because you forgot, but because the option does not exist. "You never set it" and "it cannot be set" produce the same empty cell and demand opposite actions: one sends you to the vendor, the other sends you to rewrite your rule. Collapse them into "not covered" and you lose the only information that tells you which.

Unknown is the one people get wrong, and it is the one that matters. If the page did not load, or asked for a login, or turned out to be showing a different child, the answer is unknown — never not satisfied. "We could not look" and "it is not set" are different facts. A tool that conflates them will eventually tell a parent their child is unprotected because the wifi dropped, and after the second false alarm nobody reads it again.

The bug that would have shipped green

Here is the one that still bothers me.

The rule was ninety minutes a day. The vendor's dropdown said No limit. My parser read that string, found no digits, and returned zero. Zero minutes is less than ninety. Less than ninety is stricter than you asked. Stricter counts as in force.

A child with completely unrestricted access would have rendered as a green tick, and the reasoning would have been internally consistent at every step.

This is the failure mode I've come to think is the real one in this kind of work: not the error that crashes, but the error that returns a plausible value and passes every check downstream. It fails toward reassurance. Nobody investigates a green tick.

The fix is a sentinel — "No limit", "None", "unlimited", "off" all parse to infinity, which is never less than anything, so it falls through to not in force on its own. What actually found it was setting the real thing to No limit and watching the page confidently tell me everything was fine.

Guards that can't fail aren't guards

I wrote a lot of tests for this. Then I started breaking the code on purpose to see which tests noticed.

Six of them didn't. A test for app-scoping used a rule the surface couldn't have handled anyway, so it passed for a reason unrelated to the thing it named. A check that a failed read never counts as satisfied was quietly bypassed by an empty fixture. One whole judge — the one comparing content ratings — turned out to have no tests at all; deleting two of its branches changed nothing.

A test that has never been seen to fail is a hypothesis, not a guarantee.

The version of this I liked best was the last one. Before publishing anything I wrote a leak checker: scan a fresh clone, every commit on every ref, for anything private. Its self-test plants a secret of each kind and asserts a finding, so the checker can prove it is capable of failing.

I ran it against the repository I was about to publish. It found a real child's account id — in the leak checker's own source, where I had used the real value as the example secret to plant.

The gate caught its own author, in the file whose entire purpose was to prevent that exact mistake. I have never been happier to be told I was wrong by something I wrote.

What it refuses to do

It does not watch anyone. No usage, no activity, no messages, no location. The only things it records are settings and your own confirmations.

That isn't a promise in a README. A test whitelists every field allowed to reach the page, and a new one fails the build until somebody justifies it in writing. When I added the vendor's own words to the display — so the page shows "1 Hour" beside its verdict rather than asking to be believed — that guard stopped the build and made me argue, on the record, that a control's value is not a record of use.

Governing a policy and monitoring a child are adjacent enough that the second will arrive by accident if nothing structural prevents it. Same reason as before: decide what layer you are, and say no to the adjacent one out loud and repeatedly.

The part that transferred

What surprised me is how little of this was new thinking. Separate authoring from enforcement. Stay out of the runtime path. Translate one authored intent into each system's own vocabulary. Be structurally incomplete on purpose, because that is what makes you safe to sit next to.

I argued that for a decade across dozens of enterprise systems, and the same shape holds for one family and six consumer apps. The scale changed by four orders of magnitude and the architecture didn't move.

The difference is only in who owns the enforcement points. At work I chose not to own them. At home the choice was made for me — and it turns out the discipline you adopt voluntarily is the one that saves you when it stops being optional.

The code is public, with simulated vendors so you can watch the whole loop run in two minutes without an account anywhere.

Filed underPolicy · Architecture · Building
RelatedThe Layer I Refused to Build · Which of Your Agents Is Actually Ready?