03 — Capability Model
The capability model is the foundational design artifact. It is what makes “fully local” a verifiable property rather than a marketing claim.
Principles
- Default-deny. Apps start with zero capabilities. The manifest is the only way to grant.
- Defense in depth. Three enforcement layers: macOS kernel (App Sandbox), Tauri IPC, Locara runtime. Each independently denies undeclared use.
- No ambient authority. Code can only use handles given to it. No globals like “the filesystem” — only specific scoped paths.
- Granular scoping. Not
fs: truebutfs.read: ["~/Documents/receipts/**"]. - Declarations are signed and immutable. A capability declaration in a published app is fixed for that version. Changes require a new version + re-review.
- User consent on capability change. Updates that expand capabilities trigger user re-consent + cool-down.
Capability categories
net — Network
| Value | Meaning |
|---|---|
false | No network access. Enforced by macOS sandbox (no network.client entitlement) and runtime. |
true | Unrestricted network. Triggers loss of “fully local” badge. |
{ allowed_hosts: [...] } | Outbound to declared hosts only. Wildcards supported (*.api.example.com). |
{ allowed_hosts: [...], allowed_ports: [...] } | Combined host + port. |
The “fully local” badge requires net: false.
fs — Filesystem
"fs": {
"user-selected": "read-write" | "read" | false,
"app-data": "read-write" | "read" | false,
"user-paths": {
"documents": "read",
"downloads": "read-write"
}
}
| Sub-capability | Meaning |
|---|---|
user-selected | File-picker-mediated access (macOS Powerbox). User explicitly chooses files. |
app-data | App’s own container. Always read-write by default; declare false for read-only apps. |
user-paths | Persistent access to standard user paths. Each declared path triggers a separate user prompt at install. |
For arbitrary glob patterns outside standard paths, the temporary-exception.files.user-selected entitlement is used; see macOS-specific notes below.
device — Hardware
"device": {
"microphone": true,
"camera": false,
"screen-recording": false,
"speech-synthesis": true
}
Each maps directly to a macOS TCC permission. First use triggers system prompt; user can revoke in System Settings.
models — Model dependencies
"models": [
"whisper-large-v3-q4@sha256:abc123...",
"qwen2.5-3b-instruct-q4@sha256:def456..."
]
Each entry: <model-id>@sha256:<hash>. The hash pins a specific quantization. The runtime refuses to load a model not in this list.
A model dependency implicitly grants:
- Disk space for the model
- Read access to the shared model cache for those specific files
It does NOT grant network — model fetch goes through the Locara runtime, not the app. See 09-models.md.
tools — Tool execution
"tools": [
"wasm.text-utils@1.0",
"wasm.image-resize@2.1"
]
Tools are wasm modules registered with the runtime. Each tool declares its own capability requirements (e.g., this tool reads a file and returns string output). Apps explicitly opt into specific tools.
Tools cannot exceed the app’s declared capabilities. A tool that wants network can only run in apps that declared network. See 10-tools.md.
ipc — Inter-app communication
(committed) v1 ships shared-folder IPC — a minimal, file-based mechanism for explicit data sharing between cooperating apps. No live RPC, no shared memory, no message passing. Just declared shared folders.
"ipc": {
"shared_folders": [
{
"name": "transcripts",
"mode": "write",
"shared_with": ["kingtongchoo/docvault"]
}
]
}
How it works
- App A declares a
shared_folderwithmode: "write"and a list of partner app IDs. - App B (a partner) declares a matching
shared_folderwithmode: "read". - The Locara runtime creates a real folder at
~/Library/Containers/Shared/Locara/<sha-of-app-pair>/<folder-name>/. - Both apps see this folder mapped into their containers as
$SHARED/<folder-name>/. - macOS sandbox enforces: only the declared apps can read/write; modes are respected.
- Reciprocity is required: both apps’ manifests must declare the same shared folder + the partner. Asymmetric declarations are rejected at install.
Why folders, not RPC
- Simple mental model: it’s just files.
- Apps remain otherwise isolated; no live API surface to attack.
- Asynchronous by nature — App A writes, App B reads later. No coupling.
- Aligns with the “files over apps” philosophy.
- macOS App Sandbox already supports shared containers via the App Group entitlement.
Lifecycle
- Folder is created when both apps are installed + both declare each other.
- Folder is removed when either app is uninstalled (after a 30-day grace period to allow re-install).
- Data in the shared folder is co-owned — both apps can declare ownership / contribute / consume.
- Each app’s runtime surfaces shared folders within the app’s own UI: “Transcribe shares ‘transcripts’ with DocVault. View files.”
What’s not in v1
- Live messaging / RPC between apps. Defer to v2 with a clear IPC API if demand emerges.
- Cross-publisher shared folders without explicit reciprocal opt-in. Both publishers must independently agree.
- Shared SQLite databases. Each app has its own DB; if they need shared structured data, one writes JSON / SQLite to the shared folder and the other consumes.
Use case: Transcribe → DocVault
Transcribe declares:
"ipc": {
"shared_folders": [
{ "name": "transcripts", "mode": "write", "shared_with": ["kingtongchoo/docvault"] }
]
}
DocVault declares:
"ipc": {
"shared_folders": [
{ "name": "transcripts", "mode": "read", "shared_with": ["kingtongchoo/transcribe"] }
]
}
When both are installed, DocVault sees the latest transcripts as .md files in $SHARED/transcripts/ and indexes them alongside its other documents. Single integration point; both apps remain otherwise isolated.
Mapping to enforcement layers
| Locara capability | macOS entitlement | Tauri permission | Locara runtime |
|---|---|---|---|
net: false | (omit network.client) | http: { scope: [] } | Refuse outbound calls |
net: { allowed_hosts: [...] } | network.client | http: { scope: <hosts> } | Validate against allowed_hosts |
fs.user-selected: "read-write" | files.user-selected.read-write | fs: { scope: <powerbox> } | Use security-scoped bookmark |
fs.app-data: true | (container-default) | fs: { scope: $APPDATA/* } | Allow |
device.microphone: true | device.microphone | mic: enabled | TCC prompt |
device.camera: true | device.camera | camera: enabled | TCC prompt |
models: [...] | (none) | (none) | Validate model hash, refuse others |
tools: [...] | (none) | (none) | Wasmtime: deny all WASI imports except what tool declared |
This mapping is (committed) for v1; future capabilities will be added with the same triple-layer mapping.
Capability cool-down rules
When an app updates and its manifest expands capabilities:
| Change type | Behavior |
|---|---|
New capability added (e.g., device.camera: false → true) | 7-day cool-down + user re-consent before activation |
Capability scope broadened (e.g., fs.user-paths.documents: "read" → "read-write") | 7-day cool-down + re-consent |
| New model with sensitive context (e.g., adding a vision model) | 24-hour cool-down + notification |
| Capability narrowed or removed | Immediate; no consent needed |
| New version, no capability change | Auto-update (if user enabled auto-update) |
The cool-down is enforced client-side: the new version is downloaded but the new capabilities don’t activate until the cool-down passes. User can dismiss the cool-down via explicit re-consent. This catches account-takeover attacks where a compromised publisher pushes a malicious update.
Static analysis rules
locara verify runs an AST analyzer on src/**/*.ts(x) and:
- Network calls. Any
fetch,XMLHttpRequest,WebSocket, dynamic import of remote URLs → must havenet: !== false. - File system. Any
@locara/sdkdb.*use → checks against declaredfs.app-data. Anyfs.user-selecteduse → checks declaration. - Device APIs.
navigator.mediaDevices.getUserMediaetc. → checksdevice.*. - LLM calls.
llm.chat({ model: X })→ checks model X is incapabilities.models[]. - Tool calls.
tools.invoke('name')→ checks tool is declared.
False positives possible (e.g., dead code branches). The analyzer warns but doesn’t block; reviewer (initially you) decides.
Runtime enforcement
Even if static analysis misses something, the runtime denies. Order of checks for each operation:
- Tauri IPC: is this command registered for this capability?
- Locara plugin: does the calling app’s manifest declare this capability?
- Scope check: is the specific argument (path, host, model) within declared scope?
- macOS sandbox: kernel allows the underlying syscall?
If any layer denies, the operation fails. Frontend gets a CapabilityDeniedError with the specific capability that wasn’t granted.
Locara runtime capability surface
For v1, the Locara runtime exposes these capability-gated operations:
locara.llm.chat → requires: models[] entry for the model
locara.llm.embed → requires: models[] entry
locara.transcribe → requires: models[] entry + device.microphone (live) or fs (file)
locara.vlm.describe → requires: models[] entry
locara.ocr → requires: models[] entry
locara.db.query → requires: fs.app-data (always granted by default)
locara.db.vec.search → requires: fs.app-data + storage.vector configured
locara.tools.invoke → requires: tools[] entry
locara.fs.pick → requires: fs.user-selected
locara.audio.record → requires: device.microphone
locara.image.capture → requires: device.camera
This list is the enforceable surface. Adding a new capability means adding it here, in static analysis, in the manifest schema, and in runtime checks — coordinated change across 02-manifest.md, this file, 05-sdk.md.
”Fully local” badge
Awarded automatically when:
net === false- All
models[]entries reference Locara-curated, locally-cached models - All
tools[]entries are wasm tools with no declared net capability - No
ipcreferences to apps with broader capabilities
Computed at registry-review time; displayed prominently on app card. Verifiable by user from the runtime.
Cross-references
- Manifest schema: 02-manifest.md
- High-level modality + tooling declarations that expand into capabilities: 04-modalities.md
- SDK API surface: 05-sdk.md
- Tool execution details: 10-tools.md
- macOS App Sandbox notes:
../notes/mac-app-store-sandbox.md - Capability cool-down rationale: 14-trust-safety.md