‹lenses

lenses

all docs

Lenses — scoped projections of a thing, as things

A seed + roadmap document. Written for the next agent (human or model). It captures *why* a thing can present a different, narrower face to a different viewer — and *how* we intend to deliver that with no engine primitives and no changes to this repo, purely as store content. Status: design only. Nothing here is built yet. But unlike most plans, the load-bearing claim ("this needs zero engine changes") is already proven by the code that ships today (kernel/access.cpp, kernel/resolve.cpp — see below). The lens *convention* does not exist yet; the *substrate it rides on* does.

The one idea

A lens is a scoped projection of a thing — a narrower face that exposes only some of its data, functions, and actions — expressed as a thing in the store. A lens carries *access posture* (grant_* rules) and optionally a few views, and no real data of its own. You don't carve a new class; you let one thing show a different face depending on who is looking and which lens they wear.

The payoff is the twin scenario:

My twin returns work done back to me as if I'd done it — but it worked on my own data, which I never had to hand over, because that was the section it was *supposed* to have. And the same twin can run under a low-budget lens and a full lens at the same time: same data underneath, two conditions, two realities.

The whole point is simplicity: lenses are *just things*, things already compose things, and access control is already a native property of every thing. So lenses inherit the entire feature set for free, and the engine never learns a new concept — it just evaluates whatever grant_* is present.

Why no primitives are needed (this is the proof, not a hope)

Three facts from the engine that ships today:

1. Grants are read *live* from the mutable per-request map. accessAllowed

looks up grant_<name> in g_data *at check time* (kernel/access.cpp), not from frozen class data. Anything that writes a grant_* key during a request — including a DSL action calling functions/set — reshapes access for everything that runs after it. Access control is live, not baked at load.

2. Every read already passes through the gate. resolve() runs

accessAllowed(tok, 'r') for every member read; denied → returns empty, fail-closed so views show nothing rather than crash (kernel/resolve.cpp). You wire up nothing — the gate is already on every door.

3. The owner always wins (kernel/access.cpp: principal == owner → true).

You (the human) see everything in your own twin, unconditionally. The *twin*, acting as a non-owner principal/role, sees only what its lens leaves open.

That is the entire substrate a lens needs. A lens is nothing but a thing carrying grant_* rules; the engine already does the rest.

The twin scenario, mapped exactly

• The data lives in your home (~/zero.9.data/<you>/). The twin runs *there*,

as __object = you. The data is co-located — never "handed over." This is the existing twin model (.../store_seed/home/_twin.action).

• The twin acts as a non-owner principal/role wearing a lens whose grants

exclude budget (grant_budget = owner=r, nothing for the twin). It reads its section, gets empty on budget, does the work, writes the result back.

• "Different conditions at the same time" = run two twin instances (or the

same twin under two roles) over the same home — one wearing lo-budget, one wearing full. Same data, two lenses, two realities. The engine doesn't even know "lens" is a word.

The simple shape (pure store)

• lens/defaults.data → parent = thing. Marks a thing as a lens. Optional sugar.

• A concrete lens, e.g. lo-budget/defaults.data, carries posture only:

grant_budget = owner=r, grant_payments = owner=, grant_invoices = analyst=r. No data — just the access face (and maybe a _lens view that explains itself).

• A twin wears a lens one of two zero-change ways:

• Static stack — chain lenses via parent =:

analyst → redacted → read-only → person. Each lens contributes its grants; nearest-in-chain wins on conflict (a deliberate, defined merge). "Mix of such things," done entirely in the store.

• Per-request swap — the twin wears a role; the lens-as-role scopes that

one request. Swap the role, swap the reality.

"Provided to all things by default" costs nothing extra: lenses *are* things, and access control is already native to every thing. This is dead in the grain of zero.9 — thin engine, meaning lives in the store.

Seeds already in the codebase (what we lean on, not what's built)

Seed

Location

Why it matters

Live, per-request grants

kernel/access.cpp (accessAllowed reads grant_<name> from g_data)

A DSL action can recompose access at runtime — the heart of "lens with no primitive"

Read gate on every member

kernel/resolve.cpp (accessAllowed(tok,'r'), fail-closed)

Scoping is already enforced everywhere; lenses just set the rules

Owner-always-wins

kernel/access.cpp (principal == owner)

Human sees all; the twin is the one that gets scoped

Grant declaration + inheritance

.../store_seed/thing/defaults.data, functions/core/loadchain.cpp

A lens carries grant_*; chaining lenses = composition via parent =

Role entitlement

kernel/identity.cpp, members roster (<principal>\

<role>)

"Wear a lens" = wear an entitled role

Per-object views

.../store_seed/_core/boot.action ("the viewer comes with its own views")

A lens/twin can carry its own .action overrides, first-def-wins

The twin

.../store_seed/home/_twin.action

The thing a lens is worn by

Composition — two semantics, only one is free

• Nearest-wins (a deliberately ordered lens stack via parent =): free,

zero changes. Good for pre-written lenses. Caveat: the inheritance walk is first-def-wins, so the nearest lens controls each member — it does *not* compute "most restrictive across all lenses."

• Deny-wins / narrow-only (adding any lens can only ever *narrow* — the safe

property for mixing an open set at runtime): wants a small DSL helper that reads each named lens's grants and merges by intersection. Even this is a generic "load a thing's data into this request" helper (~10 lines, useful beyond lenses), not a lens primitive in the kernel.

Recommendation: ship nearest-wins first (needs nothing); add the generic merge helper only when open-set runtime mixing is actually required.

Roadmap (phased)

Phase 0 — design (this document)

Captured here + in agent memory. The proof that the core is zero-change is the load-bearing part.

Phase 1 — twin identity (the only real blocker)

Confirm a twin can run under a distinct, non-owner principal (e.g. ?as=<twin>&role=<lens>) without local-door trust collapsing it back to owner (kernel/identity.cpp, kernel/access.cpp:68). If it already can → free. If local doors force owner → this is the *one* tweak the whole idea depends on. *Done when:* a twin reads as a scoped non-owner over the owner's own home.

Phase 2 — the lens convention + one proof example (pure store)

• A lens base class (optional) + a concrete lo-budget lens carrying only

grant_*.

• A twin object that wears it (stacked parent = or a role).

• *Done when:* the twin returns work computed over your data, with budget

invisible to it and visible to you — demonstrated end to end, no engine change.

Phase 3 — write-back

Confirm/define w grants on an output section so the twin's result lands back in your home. *Done when:* "returns work done back to me" is real, scoped both ways.

Phase 4 — transparency view

A _lens view (or extend the access panel) that shows exactly what a lens scopes, so a lens is legible to its owner.

Phase 5 — narrow-only composition (only if needed)

The generic load-a-thing's-data-into-this-request helper + an intersection merge, enabling safe runtime mixing of an arbitrary open set of lenses.

Open questions (decide before/while building)

• Twin identity (Phase 1): does ?as=<twin> already yield a non-owner

principal on local doors, or does local-door trust force owner? This gates everything.

• Composition default: nearest-wins (free) vs deny-wins (safer, needs the

helper)? Lean nearest-wins to start.

• Sections sugar: worth a way to name a *group* of members (so a lens scopes

"the budget section" instead of enumerating keys)? Pure convention if so.

• Two twins, same home, same instant: two processes/requests, or one process

multiplexing roles? (Leaning: separate requests/instances — the engine is already per-request.)

Relationship to the versioned runtime

This is orthogonal to [versioned-runtime](./versioned-runtime.md) but rhymes with it: both keep the engine thin and push capability into the store. Lenses are about *who sees which face of a thing now*; the versioned runtime is about *how the action language evolves over time*. They compose — a lens is just a thing, and things get versioned like any other.

Where this came from

A design conversation (2026-06) refining the earlier "carve a sub-class / projection" idea down to its simplest correct form: don't carve a class — let one thing wear lenses, where a lens is itself a thing carrying access posture. The insight that made it land: in zero.9, access control is already *live and native* (grants are mutable per-request data evaluated on every read), so scoped projection needs no new primitive — only store content and a way for a twin to run as a non-owner.

[VISION.md]: ../VISION.md