purposeos

Manifest: app.json

Schema 1. Unknown keys are rejected when you install and again when the executor loads the app.

Fields

KeyTypeRequiredNotes
schema1yesSchema version. Unknown keys are rejected everywhere.
id[a-z0-9-]+yesApp id. Prefix of everything the app owns (services, actions, sockets).
namestringyesDisplay name.
versionsemveryesAn install of a strictly newer version upgrades; never a downgrade.
requires.protocol1yesService protocol version. Checked at install.
ui.kindqml | web | native | noneyesweb runs in purpose-webhost; native must live under bin/; qml runs in Quickshell.
ui.entrypathMust exist in the bundle.
launcherobjecticon, categories, mime_types, show. Generates a .desktop file.
services[]object[]name, runtime (native), exec, start (session), timeout_ms, actions[].
intents[]object[]intent, action, phrases, result (capture | none), sink, view, end, discard.
names[]string[]Spoken names, so "open my diary" finds the app.
permissions.callstring[]Other services the app may call. An app may always call its own.
permissions.filesstring[]ro:~/path or rw:~/path, 1 to 8 plain components. Enforced by Landlock.
permissions.networkbooltrue is rejected in v1 (signed apps later).
permissions.micboolDeclared; the microphone is owned by the voice service.
uses[] / implements[]reservedParsed and ignored. Reserved for swappable services (action@app-id).

Example: Journal

apps/journal/app.json, shortened
{
  "schema": 1,
  "id": "journal",
  "name": "Journal",
  "version": "0.1.0",
  "requires": { "protocol": 1 },
  "ui": { "kind": "web", "entry": "ui/index.html" },
  "launcher": { "icon": "assets/journal.svg", "categories": ["Office"], "show": true },
  "services": [{
    "name": "store",
    "runtime": "native",
    "exec": "bin/journal-store",
    "start": "session",
    "timeout_ms": 30000,
    "actions": ["journal.start", "journal.append", "journal.end", "journal.list", "journal.search"]
  }],
  "intents": [{
    "intent": "journal",
    "action": "journal.start",
    "phrases": ["add to journal", "journal this", "dear diary"],
    "result": "capture",
    "sink": "journal.append",
    "view": "action:ui/Capture.qml",
    "end": "journal.end",
    "discard": "journal.discard"
  }],
  "names": ["journal", "my diary"],
  "permissions": { "call": ["journal-store"], "data": "own", "files": [], "network": false, "mic": false }
}

Rules

Intents and capture

An intent maps spoken phrases to an action. Phrases match exactly, or by content words: “add this to my journal” matches “add to my journal”, but “add milk to my journal” does not. A spoken name before a comma is ignored: “buddy, add to journal”.

With result: "capture" the next dictation goes to your sink action until the user saves or discards. The end action finishes it and discard throws it away. A capture that times out counts as a save, so an abandoned dictation is kept.

FieldMeaning
resultcapture or none
sinkAction that receives each dictated segment. Required for capture.
viewaction:ui/Capture.qml, a QML file the shell shows as the dictation panel.
end, discardActions for Save and Discard. Capture only.

What the installer says

These are the real messages from the validator for three broken manifests.

An action that is not yours
app.json: services[0].actions[0]: "other.greet" is not owned by this app: actions must start with "hello." (or be a declared intent name)
Network access
app.json: permissions.network: network access is not supported in protocol 1 and apps requesting it are refused (planned for signed apps)
An unknown key
app.json is not valid: unknown field `bogus`, expected one of `call`, `data`, `files`, `network`, `mic`

What install generates

For each service the installer writes an executor manifest, a session start hook and a ledger entry. You never write these.

per user
~/.local/share/purpose/apps/<id>/            code (read-only) and data/
~/.local/share/purpose/plugins/<id>-<svc>/   executor manifest
~/.local/share/purpose/session.d/6N-<id>-<svc>   start hook, restarts the service with backoff
~/.local/share/applications/purpose-<id>.desktop
~/.local/share/purpose/ledger/<id>.json      exactly what to remove on uninstall
generated executor manifest for hello
{"abi":"service","abi_version":1,"actions":["hello.greet"],"entry":"hello-svc.sock","kind":"runtime","name":"hello-svc","timeout_ms":10000,"version":"0.1.0"}