‹native-capabilities

native-capabilities

all docs

Native capabilities — the device is a class, each feature is a catalog row

How a Zero scene reaches a real device capability — camera, location, haptics, share, dialer,

clipboard, biometrics, files — through one uniform, data-driven mechanism, not one bespoke

node per feature. Every hardware/OS feature is a capability = one row in a catalog. Any action

emits it by name through inherited helpers; the platform shell that draws the scene supplies the

method through a small set of generic dispatchers keyed by kind. So adding a feature is a

catalog row, not code (except the rare bespoke live view).

>

Status. The store side is built & verified here: the capability catalog (65 rows), the

inherited thing._native_* helpers, the os browser class (os.content / os.cap / os.result

/ os.unsupported) and the platform subclasses, and trust.scan migrated onto _native_view. The

shells' generic per-kind dispatchers are added on the existing bridge=/onNative seams (the one

intended code change per platform) and build in CI / need a device — see *Honesty* and

*Verification* below.

>

Companion to [native-rendering.md](native-rendering.md) (the engine emits scenes, the viewer

draws) and [ARCHITECTURE.md](ARCHITECTURE.md) / [VISION.md](VISION.md). Keep in sync when you

touch os/capabilities.jsonl, a thing/_native_*.action helper, a native/one-shot button node,

or a shell dispatcher (mobile/android/.../NativeBridge.kt, iOS SceneRenderer, desktop shells).

The one idea

Everything in Zero already follows one shape: a folder is a class, an .action is a method.

A device's native powers are no different — the device is a class, and each hardware/OS feature is a

capability. A capability is *one row* in the catalog os/capabilities.jsonl. The DSL never says

"use the Android camera"; an action names a capability (share.text, camera.qr, geo.once) and

whichever platform is drawing supplies the method.

The payoff is that adding a feature is a catalog row, not code. A row carries everything the

shell needs to execute the feature on every platform. The shell learns, *once per platform*, how to

run each kind of capability; from then on a new feature of that kind is pure data.

graph LR

ROW["os/capabilities.jsonl<br/>1 row = 1 capability<br/>(kind + per-platform descriptor)"] --> H["thing._native_do / _native_view<br/>(inherited by every class)"]

H --> W["scene wire<br/>button(bridge,cap,capkind,p1..3,and/ios/mac/win/lin)<br/>or native(cap,p1..5,and,ios) + fallback kid"]

W --> D["shell dispatcher, keyed by kind"]

D -->|intent| OS1["OS intent / URL / opener"]

D -->|effect| OS2["vibrate · clipboard · notify · toast · torch · tts"]

D -->|value| OS3["clipboard.read · geo · battery · info · network · biometric"]

D -->|view| OS4["registry: cap -> native view (camera.qr, web.view)"]

W -.->|web / no bridge / unknown cap| FB["fallback: row.fallback URL or the native node's kids"]

The catalog — the single source of truth

os/capabilities.jsonl is the one place every OS/hardware feature lives — one compact JSON object

per line:

{"id":"share.text","title":"Share text","kind":"intent","area":"share",

"desc":"Open the OS share sheet with some text.",

"params":["text"],"platforms":["android","ios","mac","linux","windows"],

"android":{"action":"android.intent.action.SEND","type":"text/plain",

"extras":{"android.intent.extra.TEXT":"{1}"}},

"ios":{"kind":"share","items":["{1}"]},

"fallback":"/os/unsupported?cap=share.text"}

Field

Meaning

id

the capability name, namespaced area.verb — the contract every caller uses

title / desc

human label + one-line description (drives the browser pages)

kind

view \

intent \

effect \

value — selects the shell dispatcher

area

grouping (camera, share, comms, location, system, clipboard, …)

params

parameter names, in order — they bind to placeholders {1} {2} {3}

platforms[]

which platforms back this row natively (membership = the platform name is in this array)

<platform> blocks

android / ios / mac / windows / linux — the descriptor the shell executes

fallback

where a non-backing shell (or the web) degrades to — a helpful page or a plain URL

Kinds.

• view — a live view drawn *in the layout* (camera preview/scan, embedded map/web view). Emits a

native scene node; the shell fills a host view.

• intent — hand off to the OS: fire an Android intent, open a tel:/mailto:/geo: URL, or

the desktop opener. Some (returns:true) come back with a value.

• effect — a fire-and-forget device effect: vibrate, write clipboard, notify, toast, torch, TTS,

app.update (the shell installs the newer build of itself). That last one is the argument for

this whole mechanism in one row: /node/updates is ONE screen that has to work on a laptop, where

the engine is a file the node fetches and re-execs into, and on a phone or a TV, where it is inside

the APK and only the shell can replace it. The DSL names the capability; each platform supplies the

method; a viewer with no bridge falls back to the row's /download. Nothing about the update itself

moves into the catalog — Updater is still the only downloader and the system installer the only

installer.

• value — read something and post it back: clipboard text, location, battery, device info,

network status, biometric result.

{1..3} param binding. params names map positionally onto {1}, {2}, {3} inside the

platform descriptors. A caller supplying p1="Hello" makes the shell substitute {1}→Hello

everywhere in that platform's descriptor before executing. Empty params substitute as empty strings.

An optional "perm" inside a platform block names the OS permission to request first.

The inherited thing._native_* helpers — mostly on actions

The catalog is reached through three helpers on the root thing class, so any class inherits them

and can reach any capability by name, with no code:

Helper

Emits

Use

[thing._native_spec](../thing/_native_spec.action)

*(nothing)* → returns the catalog row for an id

the lookup every emitter uses; matches by id via a foreach + _native_match that sets a global on the first hit

[thing._native_do](../thing/_native_do.action)

a one-shot button node

intent / effect / value capabilities

[thing._native_view](../thing/_native_view.action)

a live-view native node (opens *and* closes the container)

view capabilities

Because they live on thing, any action anywhere writes, e.g.:

_native_do "share.text" "Share text" "/os/result" "Hello from Zero" "" ""

_native_view "camera.qr" "/trust/scanresult" "Scan a code" "/trust/scan?raw=1" ""

_native_do $1=cap $2=label $3=result $4..$6=p1..p3 resolves the spec, reads its kind and the

per-platform descriptors, and emits the button. _native_view $1=cap $2=result $3=title $4=fallback

$5..$9=p1..p5 emits the native node with the fallback as its kid. Neither helper knows anything about a

specific feature — they just project a catalog row onto the wire.

The emitted scene wire — VERIFIED

One-shot capability → a button node (rides the existing bridge= seam)

_native_do emits (verified via os.cap?id=share.text):

{"type":"button","props":{

"label":"Share text","kind":"secondary",

"bridge":"share.text","cap":"share.text","capkind":"intent",

"result":"/os/result",

"p1":"Hello from Zero","p2":"","p3":"",

"and":"{\"action\":\"android.intent.action.SEND\",\"type\":\"text/plain\",\"extras\":{\"android.intent.extra.TEXT\":\"{1}\"}}",

"ios":"{\"kind\":\"share\",\"items\":[\"{1}\"]}",

"mac":"","win":"","lin":"",

"action":"/os/unsupported?cap=share.text"

}}

• and / ios / mac / win / lin are the per-platform descriptor as a JSON string (parse

it). Empty string = this platform doesn't back the row.

• The shell binds {1}→p1, {2}→p2, {3}→p3 inside its descriptor, then executes by

capkind. result is where a returning cap posts its output (result?value=<urlencoded>).

• action is the web / no-bridge fallback — the row's fallback (a helpful page or a plain URL),

so the button still does something useful without a native shell.

Live-view capability → a native node (rides the existing onNative seam)

_native_view emits (verified via trust.scan):

{"type":"native","props":{

"cap":"camera.qr","result":"/trust/scanresult","title":"Scan a zero sign-in code",

"p1":"","and":"{\"view\":\"camera.qr\",\"perm\":\"android.permission.CAMERA\"}","ios":""},

"kids":[{"type":"webview","props":{"src":"/trust/scan?raw=1"}}]}

• The shell's SceneRenderer.buildNative hands (cap, node, host) to its NativeBridge registry,

which fills the host with the live view.

• kids are the fallback. A viewer with no native bridge, or one whose registry lacks cap,

renders the kids instead (the renderer's *unknown-type → render kids* branch) — so a native node

degrades to a webview / paste form / plain message on web and older shells, and auto-upgrades to

the real capability the moment a shell ships it. Forward- and backward-compatible by construction.

The shell "touch once" — generic dispatchers keyed by kind

The shell wires each kind *once*; thereafter a new feature is a catalog row. The existing seams

stay:

• onNative(cap, node, host) -> Bool for view caps — the registry. Bespoke views (camera.qr)

key off cap; a generic view dispatcher reads and.view == "web.view" + and.url and hosts a

WebView. Unknown/absent → render the fallback kids.

• The button bridge= seam for one-shots. When props.capkind ∈ {intent, effect, value}, the

shell runs the matching generic dispatcher (binding {1..3} = p1..p3 from *this* platform's

descriptor); otherwise it falls through to the shell's existing verb handling (e.g. the auth verbs).

The three one-shot dispatchers:

1. intent — build & fire the OS intent / URL / opener from the descriptor. If returns:true,

capture the result and navigate result?value=…. Descriptor keys: action, data, type,

extras, launchPackage, returns (Android); {"kind":"share"|"url", …} (iOS);

{"open":…} / {"exec":[…]} (mac/linux/windows).

2. effect — a small keyed switch: vibrate · clipboard.write · notify · toast ·

flashlight · tts. Reusing an existing key for a new row is zero code; a genuinely new key

is one branch.

3. value — a small keyed switch: clipboard.read · geo.once · battery · device.info ·

network · biometric; read → navigate result?value=….

Permissions. Request the descriptor's "perm" via the platform's runtime-permission flow before

acting; on denial, show a message or navigate the fallback. The three-gate model (capability installed

• OS permission + zcap) from [ARCHITECTURE.md](ARCHITECTURE.md) still holds.

The os browser + platform classes — "the device is a class"

The catalog is *browsable*, and every page is built entirely from os/capabilities.jsonl — so it

grows with the catalog and never needs editing:

Class / action

What it does

[os / os.content](../os/content.action)

the master catalog: every capability grouped by area (camera, comms, control, …) via os/_cap_row — the catalog is area-sorted and a heading is emitted on each area change, so it stays navigable as it grows

[os.cap?id=<cap>](../os/cap.action)

one capability's detail + an interactive try — a view cap embeds _native_view; a one-shot renders an inputs form (per the row's params) that POSTs back, baking the real values into the live _native_do button so you invoke it for real

[os.result?value=…](../os/result.action)

the neutral sink where a returning cap (value, or returns:true intent) posts its output

[os.unsupported?cap=<id>](../os/unsupported.action)

the honest fallback page a device-only cap degrades to on the web / a shell that doesn't back it

[os.manifest](../os/manifest.action)

the machine-readable capability manifest (ndjson) — the *"this device declares what it can do"* bridge a shell/peer/tool fetches to discover every capability

Platform subclasses. Each platform is an os subclass — android, ios, mac, windows,

linux, tv — that answers *"if this is an Android, what can it reach?"*. The page is built from the

same catalog via [os._plat_row](../os/_plat_row.action), which marks each row native or

fallback purely by testing whether the platform's name is in that row's platforms[] array. No

per-platform capability list is hand-maintained: *"a platform is just another class, and each native

feature is one method it implements."* (android is live today; the rest follow the identical

pattern.)

Capability catalog (63 rows)

The catalog currently holds 63 capabilities — 7 view, 30 intent, 14 effect, 12 value. "Native

on" lists the platforms whose platforms[] includes that platform; every other platform (and the web)

uses the row's fallback. The table is generated from the rows — the catalog itself is authoritative.

view (7) — a live view in the layout (native node)

Capability

Area

Params

Native on

camera.qr

camera

result

android

camera.capture

camera

result, mode, facing, torch, seconds, guide

android

mic.capture

media

result, seconds, guide

android

web.view

web

url

*(none yet — generic view dispatcher pending)*

geo.track

location

session, stop, interval_seconds, last_sequence, resume_state

android

app.list

apps

—

android, tv

sensor.motion

sensor

result, seconds, guide

android

app.list is rendered by the one shared Android package reader/view as a searchable icon grid;

Zero's own APK is excluded because its declared experiences occupy the launcher’s Zero-app section.

intent (30) — hand off to the OS

Capability

Area

Params

Native on

share.text

share

text

android, ios

share.url

share

url

android, ios

dial.number

comms

number

android, ios

call.number

comms

number

android

sms.compose

comms

number, body

android, ios

email.compose

comms

to, subject, body

android, ios, mac, linux, windows

web.open

web

url

android, ios, mac, linux, windows

map.show

location

query

android, ios

map.navigate

location

query

android, ios

calendar.add

calendar

title, description

android

contacts.pick

contacts

— (returns:true)

android

files.pick

files

mime (returns:true)

android, ios, mac

files.open

files

uri

android, mac, linux, windows

app.launch

apps

package

android

store.open

apps

package

android

settings.open

system

screen

android

speech.recognize

media

— (returns:true)

android

intent.raw

system

action, data, type

android

web.search

web

query

android, ios, mac, linux, windows

whatsapp.chat

comms

number, text

android, ios, mac, linux, windows

calendar.view

calendar

—

android

contacts.add

contacts

name, phone

android

alarm.set

system

hour, minutes, message

android

timer.set

system

seconds, message

android

camera.photo

camera

— (returns:true)

android

camera.video

camera

— (returns:true)

android

mic.record

media

— (returns:true)

android

app.info

apps

package

android

app.uninstall

apps

package

android

wallpaper.set

system

—

android

*Typed extras:* an intent extras value may be {"type":"int|long|bool|float","val":"{1}"} — the shell binds it as that type (string by default). This unlocks alarm/timer/typed-settings intents as pure catalog rows.

effect (14) — a fire-and-forget device effect

Capability

Area

Params

Native on

haptics.vibrate

system

ms

android, ios

clipboard.write

clipboard

text

android, ios, mac, linux, windows

notify.local

notify

title, body

android, ios, mac, linux, windows

toast.show

system

text

android

flashlight.on

system

—

android

flashlight.off

system

—

android

tts.speak

media

text

android, ios, mac, linux, windows

app.pin

apps

app, title, icon

android

a11y.tap

control

xy

android

a11y.swipe

control

path

android

a11y.text

control

text

android

a11y.back

control

—

android

a11y.home

control

—

android

a11y.recents

control

—

android

value (12) — read a value and post it to result

Capability

Area

Params

Native on

clipboard.read

clipboard

—

android, ios, mac, linux, windows

geo.once

location

—

android, ios, mac, web

battery.level

power

—

android, ios, mac, linux, windows

device.info

system

—

android, ios, mac, linux, windows

network.status

system

—

android, ios, mac, linux, windows

biometric.verify

biometric

reason

android

a11y.tree

control

—

android

screen.shot

control

—

android

contacts.all

contacts

—

android, ios, mac

perm.status

permissions

permission

android

perm.request

permissions

permission

android

perm.request.all

permissions

—

android

Adding a new capability

For intent / effect / value the whole change is one catalog row — no DSL and no shell code (as

long as an effect/value reuses an existing key). To add, say, *"open WhatsApp chat"*:

1. Append one line to os/capabilities.jsonl:

json

{"id":"whatsapp.chat","title":"WhatsApp a number","kind":"intent","area":"comms",

"desc":"Open a WhatsApp chat with a phone number.","params":["number"],

"platforms":["android","ios"],

"android":{"action":"android.intent.action.VIEW","data":"https://wa.me/{1}"},

"ios":{"kind":"url","url":"https://wa.me/{1}"},

"fallback":"https://wa.me/{1}"}

2. Emit it from any action: _native_do "whatsapp.chat" "Message on WhatsApp" "/os/result" "15551234" "" "".

That's it. It appears in os.content, gets a live try at os.cap?id=whatsapp.chat, is marked

*native* on the Android/iOS platform pages, runs through each shell's intent dispatcher on a

device, and degrades to https://wa.me/… on the web — with zero engine, shell, or helper edits.

A new effect/value is likewise row-only when it reuses an existing dispatcher key

(vibrate, clipboard.read, …); a genuinely new key adds one branch to that platform's switch.

camera.qr — the bespoke live view

camera.qr is the one built view capability and the template for bespoke views. On Android it is

CameraX preview + frame analysis decoded by ZXing core (pure-JVM — no Play Services, so it works

on de-Googled phones, Android TV, and offline). On the first decode it unbinds the camera and

routes the value exactly like the webview fallback (trust/scan.html):

• a claim/login link (/trust/claim…, or any http(s) URL) → the web door, because the

key-import ceremony needs WebCrypto;

• a device join code (zjoin:<pubkey>, or a link carrying pubkey=) → the node's result

action (trust.scanresult → confirm → trust.approve_device).

A camera-less device or denied permission falls to a message; the scene's webview fallback still

offers paste-the-code. CAMERA is runtime-requested on first use and not required to install (a

camera-less TV still runs). A bespoke view like this is the *only* case that needs native code —

you can't call a live camera or map from JSON — and it costs one native view method + one registry

line; the registry stays uniform. The generic web.view view (a WebView at a url) needs no bespoke

method at all.

One catalog, every shell, every universe — the doctrine

This is not a contacts fix; it is the rule for every device/OS feature, on every shell, across

every universe (every scoped network/fleet on a ZeroID — office, home, a private workspace all run

the same store). A capability is declared once as a catalog row and executed by each shell's

generic per-kind dispatcher. The catalog ships *in the store*, so every node on every network reaches

the same capability by the same name; a shell teaches itself a kind once, and every future

feature of that kind is pure data. There is exactly one place a feature lives (the catalog) and

one place a kind is taught (the dispatcher) — never a bespoke, one-platform path.

Why this rule exists — the contacts lesson. "Add from contacts" was first built the WRONG way: a

bespoke native node plus a hand-written Android-only reader (ContactsPickCapability). It

worked on exactly the one platform someone coded and silently vanished on iOS (no native case)

— the classic "works here, invisible there" failure the *two users, one door* doctrine forbids. The

fix was NOT to hand-write iOS, then mac, then the next platform forever. It was to see the

interaction's shape: "read a list from the device" is the value kind. Reclassified as the

contacts.all value row it became one branch per shell (Android/iOS/mac) on the *existing* value

dispatcher, an automatic /me/invite degrade on web/linux/windows, and the mold for every future

device-list read (photos, calendar, files) — which are now catalog rows, not new native code.

Classify, don't hand-code the platform. Almost every device feature is one of the four kinds

(intent/effect/value/view). A feature that fits a kind is a catalog row → cross-platform for free.

Native code is needed only to teach a shell a genuinely new *kind* — done once, after which every

future feature of that kind is data. A bespoke, single-platform capability is a defect, not a

shortcut (see the reuse-first mandate).

The test that makes it stick. [roadmap/capability-parity/verify-cap-parity.mjs](../roadmap/capability-parity/verify-cap-parity.mjs)

asserts, for every catalog row and every platform it claims in platforms[], that the shell actually

implements it — and flags any bespoke device path that is not a catalog row. One test, every

feature, forever: a feature that skips the catalog, or claims a platform it never wired, fails CI

instead of a user hitting the hole months later.

Honesty — what is code-free vs. what needs a native method

• intent — fully data-driven. A new feature is a catalog row, no shell code.

• effect / value — a *finite keyed set*. A new row reusing an existing key is code-free; a

genuinely new key is one branch in that platform's dispatcher switch.

• view — each bespoke live view (camera, map, AR) needs one native method + one registry

line per platform. This is inherent: you cannot summon a camera preview from JSON. The generic

web.view is the exception (one shared WebView dispatcher).

• Shells build in CI / need a device. The store side above is built and verified here (renders

observed directly). The shells' per-kind dispatchers are added on the existing bridge=/onNative

seams; the mobile APK is an NDK/C++ build that runs in CI (.github/workflows/mobile.yml) and live

hardware features (camera, biometric, torch) need an on-device test. Treat shell changes as

written + structurally checked until a build/device run confirms them — never claimed verified

before that.

Verification & boundaries

• Store / DSL side (verified here): os.content builds the full catalog list from the jsonl;

os.cap emits the real one-shot button (bridge/cap/capkind/p1..3/and/ios/… + the

fallback in action) for intent/effect/value and the live-try native node for view caps;

trust.scan emits the native node (cap/result/and/ios + the webview fallback kid) via

_native_view; the platform pages mark rows native/fallback from platforms[]. Rendered scenes

were inspected directly.

• Shell side (written; CI + device pending): the Android shell implements the wire above — the

generic intent/effect/value dispatchers (NativeOneShot.kt, incl. typed intent extras and the contacts.all value key that retired the bespoke ContactsPickCapability), the

view registry (NativeBridge.kt) with camera.qr (QrScanCapability) + app.list

(NativeAppList.kt), the device-control AccessibilityService (ZeroA11yService.kt — the a11y.*

effect/value keys), and screen capture (ScreenCaptureService.kt — screen.shot via MediaProjection),

plus the matching manifest permissions/services/queries. iOS (SceneRenderer.swift) does intent

share/url + effect vibrate/clipboard.write/notify/tts + value clipboard.read/device.info/battery/network/contacts.all; desktop

(scene_mac.h / main_gtk.c / main_win.cpp) does intent open/exec + effect clipboard.write/notify/tts + value clipboard.read/device.info/battery/network (mac also value contacts.all; linux/windows degrade to the row fallback). All are structurally in place on the existing

seams but are not claimed verified until the CI APK/IPA/desktop build and an on-device run

succeed. Device-specifics to confirm: runtime-permission prompts; the a11y service enablement +

gesture dispatch + node-tree read; and screen.shot's Android-10+/14+ foreground-service timing +

ImageReader rowStride handling.

• New platforms become native by implementing the same per-kind dispatchers and honoring the same

catalog rows — no engine or DSL change. Same capabilities, different native methods: the device

class, re-implemented.