{
  "uid": "cogitave.learn.build-a-diyar-solution.the-app-package",
  "kind": "moduleUnit",
  "href": "/modules/build-a-diyar-solution/the-app-package/",
  "title": "The app package and the permission model",
  "summary": "",
  "type": null,
  "products": [],
  "roles": [],
  "levels": [],
  "subjects": [],
  "headings": [
    "Two artifact classes",
    "A package is a directory of three signed documents",
    "Zero evaluable constructs",
    "Three tiers, none of them implied by another",
    "Authority is an offline intersection: A n B n C",
    "Tier C: a firmware-compiled actuation allowlist",
    "How each tier is actually enforced",
    "What &quot;deploying&quot; means versus what &quot;enabling actuation&quot; means"
  ],
  "source": "What ships to a device is not code; it is three signed documents and a firmware table that decides what they are allowed to mean together. This unit covers the package shape, the permission tiers, and the offline computation that turns three signatures into one authority.\n\n## Two artifact classes\n\nDiyar draws one hard line: certified logic ships only inside the signed static binary (class A - the verdict engines from the previous unit, the safety typestate, the evidence codec), and everything else ships as configuration that *selects among it* (class B - the app package). A compliance device that could be taught new behavior by a file it downloads would have no certifiable behavior at all - the same split a safety PLC makes between its certified logic and its configuration.\n\n## A package is a directory of three signed documents\n\nThe unit of deployment is the directory named by `DIYAR_APP_DIR`. The older, profile-only `DIYAR_PROFILE_PATH` env var is retired, with no compatibility shim. The directory holds exactly three files, each a JSON payload with a detached signature under its own domain-separation tag, each signed by a **different key**:\n\n| File | Type | Signing key |\n| --- | --- | --- |\n| `manifest.json` | `SignedAppManifest` | the app publisher's key |\n| `grant.json` | `SignedAppGrant` | Diyar's authority key - a separate key from the manifest |\n| `profile.json` | `SignedDeviceProfile` | the profile key (the device profile from the earlier units, unchanged) |\n\nThere is no archive entry point, no interpreter, and no code anywhere in the package.\n\n## Zero evaluable constructs\n\nThe manifest is data, not a program. It contains no generic JSON value type, no expression string, no template with substitution, no regex, no URL that is ever fetched, no condition, and no arithmetic - the only way it refers to behavior is by *naming* an entry in the `CERTIFIED_ENGINES` table from the previous unit. This is enforced, not just asserted: a test greps the manifest type's declaration for the tokens `Value`, `expression`, `template`, `regex`, `url`, `script`, `condition`, and `eval`, and every document uses strict, unknown-field-rejecting parsing. The one free-shaped field, `labels`, is a flat string table with a bounded key charset and value length - nowhere for an expression to hide.\n\n## Three tiers, none of them implied by another\n\n```rust\nenum Capability { Observe, Operate, Actuate(ActuateScope) }\nstruct ActuateScope { role: ActuatorRole, index: u8 }\n```\n\n- **OBSERVE** - read signals.\n- **OPERATE** - non-hazardous state: UI, reports, setpoint *proposals*, export.\n- **ACTUATE** - command hazardous energy, scoped to a specific actuator role and index.\n\nThe tiers are deliberately **not implicative**: holding ACTUATE does not imply OBSERVE, and holding one tier grants nothing in another. Membership in each tier is explicit, which keeps the authority computation a plain set intersection with no precedence rule to get wrong. Adding a fourth tier is a visible act - a test pins the tier set, and an exhaustive match over it fails to compile if a variant is added without being handled everywhere.\n\n## Authority is an offline intersection: A n B n C\n\n```rust\npub fn effective_authority(\n    manifest: &AppManifest, grant: &AppGrant, profile: &DeviceProfile,\n    device: &DeviceIdentity, now_unix_s: u64,\n) -> Result<AuthoritySet, AuthorityError>\n```\n\nThis function is pure - no I/O, no network, no clock of its own (the time is injected) - so a device that has never reached the cloud computes the exact same answer from the three signatures alone. It runs in order, fail-closed:\n\n1. **grant freshness** - the current time must fall inside `[issued_at, expires_at)`, within one declared clock-skew allowance;\n2. **scope binds this install** - the app id and app version must match exactly, the device type must be listed, the tenant must match enrollment, and the device id must be listed when the grant narrows to specific devices;\n3. **candidate = grant n manifest** - B n A: what the publisher's manifest *requests*, intersected with what Diyar's grant *allows*;\n4. **tier C** - every surviving Actuate capability is additionally checked against the firmware-compiled actuation allowlist, below;\n5. an **empty result is a hard refusal** (`NoAuthority`) - the device does not fall back to running the app read-only.\n\n**A is a ceiling, never a grant.** An app that requests less than it was granted gets less; an app whose publisher edits the manifest to request *more* gains nothing, because the grant is a document only Diyar's offline key can sign - the same property a mobile OS's provisioning profile has over an app's own entitlements request. Revocation is two-sided: a monotonic sequence floor persisted on the device means a captured older grant cannot be replayed after a narrowing, and `expires_at` freshness means a device that goes dark loses an app's authority offline, with no cloud round trip needed.\n\n## Tier C: a firmware-compiled actuation allowlist\n\nHolding ACTUATE in both the manifest and the grant is still not enough to reach hazardous energy. A third table, compiled into the signed binary, has to name the exact pairing:\n\n```rust\npub const CERTIFIED_ACTUATIONS: &[ActuationEntry] = &[\n    // one row per (app id, actuator role, index) permitted to command hazardous energy\n    ActuationEntry {\n        app_id: \"diyar.app.<certified-actuating-app>\",\n        role: ActuatorRole::HeatSource,\n        index: 0,\n    },\n];\n```\n\nToday this table has a single real entry - the first certified heat-treatment solution, covered in its own dedicated unit; every other product onboarded so far is non-actuating and never appears here. This table is linked into the binary as constant data - there is no file, environment variable, profile field, manifest field, grant field, or cloud message that can add to it, because the device has no loader and no plugin path for it to travel through. The production authority computation takes no allowlist parameter at all; it reads the constant directly, so no caller can pass it a wider table. An actuation that is requested and granted but is not in this table is a hard `ActuationNotCertified` refusal at activation - deliberately not a silent drop, because an app that looks installed but cannot do its job is the worse failure.\n\nThe table is keyed by app id alone, not by app id and version - a deliberate choice: version-pinning here would turn every app patch into a firmware release. Version-level control belongs to the grant instead, which is revocable, expiring, and sequence-floored.\n\n## How each tier is actually enforced\n\n| Tier | Enforcement point | Mechanism |\n| --- | --- | --- |\n| OBSERVE | telemetry/reading promotion | a signal not listed in the manifest is never promoted |\n| OPERATE | HTTP/kiosk request handling | checked at request time |\n| ACTUATE | construction of the safety typestate | a value that does not exist |\n\nACTUATE's enforcement is structural rather than a runtime branch: without a granted, certified actuation handle, the composition root simply never constructs the armed safety state and never hands over the relay bank - there is nothing to check because there is nothing to actuate with. An app that holds only OBSERVE/OPERATE authority is, on this build, a device that will not start a session at all - an honest refusal rather than a session that silently never actuates while still writing evidence.\n\n## What \"deploying\" means versus what \"enabling actuation\" means\n\nSigning and placing three JSON documents in `DIYAR_APP_DIR` is a complete, self-contained deployment - no build, no compile step, no review of application logic. **Certifying a new app's ability to energize hazardous output is a different, and much heavier, act**: it requires an entry in the firmware-compiled `CERTIFIED_ACTUATIONS` table, which means a new signed binary on the existing OTA train, which means a firmware release and the certification event that goes with it. Those are deliberately two different costs, because they are two different classes of risk.\n\n> [!IMPORTANT]\n> A device offline past its grant's `expires_at` loses that app's authority - correct as a security property, but an availability trade an operator has to size deliberately: expiry should be set long relative to the worst credible offline window, with a loud local alarm well before it lapses.\n",
  "partOf": "cogitave.learn.build-a-diyar-solution",
  "durationInMinutes": 8,
  "quiz": null
}