Locara

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

  1. Default-deny. Apps start with zero capabilities. The manifest is the only way to grant.
  2. Defense in depth. Three enforcement layers: macOS kernel (App Sandbox), Tauri IPC, Locara runtime. Each independently denies undeclared use.
  3. No ambient authority. Code can only use handles given to it. No globals like “the filesystem” — only specific scoped paths.
  4. Granular scoping. Not fs: true but fs.read: ["~/Documents/receipts/**"].
  5. Declarations are signed and immutable. A capability declaration in a published app is fixed for that version. Changes require a new version + re-review.
  6. User consent on capability change. Updates that expand capabilities trigger user re-consent + cool-down.

Capability categories

net — Network

ValueMeaning
falseNo network access. Enforced by macOS sandbox (no network.client entitlement) and runtime.
trueUnrestricted 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-capabilityMeaning
user-selectedFile-picker-mediated access (macOS Powerbox). User explicitly chooses files.
app-dataApp’s own container. Always read-write by default; declare false for read-only apps.
user-pathsPersistent 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_folder with mode: "write" and a list of partner app IDs.
  • App B (a partner) declares a matching shared_folder with mode: "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 capabilitymacOS entitlementTauri permissionLocara runtime
net: false(omit network.client)http: { scope: [] }Refuse outbound calls
net: { allowed_hosts: [...] }network.clienthttp: { scope: <hosts> }Validate against allowed_hosts
fs.user-selected: "read-write"files.user-selected.read-writefs: { scope: <powerbox> }Use security-scoped bookmark
fs.app-data: true(container-default)fs: { scope: $APPDATA/* }Allow
device.microphone: truedevice.microphonemic: enabledTCC prompt
device.camera: truedevice.cameracamera: enabledTCC 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 typeBehavior
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 removedImmediate; no consent needed
New version, no capability changeAuto-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:

  1. Network calls. Any fetch, XMLHttpRequest, WebSocket, dynamic import of remote URLs → must have net: !== false.
  2. File system. Any @locara/sdk db.* use → checks against declared fs.app-data. Any fs.user-selected use → checks declaration.
  3. Device APIs. navigator.mediaDevices.getUserMedia etc. → checks device.*.
  4. LLM calls. llm.chat({ model: X }) → checks model X is in capabilities.models[].
  5. 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:

  1. Tauri IPC: is this command registered for this capability?
  2. Locara plugin: does the calling app’s manifest declare this capability?
  3. Scope check: is the specific argument (path, host, model) within declared scope?
  4. 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 ipc references to apps with broader capabilities

Computed at registry-review time; displayed prominently on app card. Verifiable by user from the runtime.

Cross-references