Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 7 additions & 55 deletions docs/tdeck-app-catalog-schema.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,17 @@
# T-Deck App Catalog Schema

This is the first Network App Store increment. It defines and validates the
future `index.json` shape, but it does not fetch, download, install, update, or
uninstall packages yet.
This page is kept as a compatibility pointer for older roadmap links.

Catalog indexes are expected at:
The canonical network App Store contract is now:

- `/sd/limitlezz/catalog/index.json`
- `/appfs/catalog/index.json`
- `docs/tdeck-network-app-catalog.md`
- `docs/examples/app-catalog-index.json`
- `scripts/validate_app_catalog.py`

The index must fit in `4096` bytes and use this top-level shape:

```json
{
"schema": "limitlezz.app_catalog.v1",
"updated": "2026-06-18T00:00:00Z",
"apps": []
}
```

Each app entry is bounded and fail-closed:

```json
{
"id": "weather.mesh",
"name": "Weather Mesh",
"version": "0.1.0",
"author": "Limitless",
"description": "Local weather reports",
"icon": "weather",
"hue": 48,
"api_version": "0.1",
"compatibility": "tdeck",
"permissions": ["display", "network_wifi"],
"download_url": "https://apps.example.invalid/weather.mesh.zip",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"size": 32768,
"screenshots": ["https://apps.example.invalid/weather.bmp"]
}
```

Validation rules:

- `id` uses the same safe token rules as local app manifests.
- `api_version` must be supported by the local SDK compatibility gate.
- `permissions` must use the existing allowlist.
- `download_url` and optional `screenshots` must be `http://` or `https://`
URLs without whitespace or control characters.
- `sha256` must be exactly 64 hex characters.
- `size` must be nonzero and no more than `2 MB` for this first package path.
- `hue`, if present, must be `-1` or `0..359`.
- The catalog can list up to `24` apps.

Serial diagnostics:
Firmware validates and loads the canonical `limitlezz.app.catalog.v1` schema.
Serial diagnostics remain:

```text
app catalog status
app catalog test
```

`app catalog status` validates a cached index if one exists and otherwise
reports that no cached catalog is present. `app catalog test` runs a built-in
valid/invalid schema selftest so hardware smoke can prove the parser without
requiring Wi-Fi or SD setup.
16 changes: 3 additions & 13 deletions docs/tdeck-feature-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,7 @@ Status labels:
| Settings | Functional/Partial | network toggles, Wi-Fi, in-place brightness slider updates, time, system, touch calibration, Developer Mode, versioned `settings.cfg` persistence with v1/v2/v3 selftest coverage | Hardware latency pass still needed. |
| Wi-Fi setup | Functional, needs validation | async scan/connect, saved SSID/password, auto-connect | Credentials are plaintext on SD; only one saved network. |
| System/battery page | Functional/Partial | live stats and battery arc | Hardware values need calibration/validation. |
| App Store | Prototype/Partial | `LZ_STORE` static catalog remains; local manifests from SD/appfs are scanned, listed as installed local apps, open a manifest detail shell, show storage quota usage, clear scoped app data on request, launch into the SDK 0.1 foreground shell, explicitly terminate foreground sessions on Close/Esc, expose bounded foreground actions with scoped storage counters plus read-only `{time}`/`{battery}` tokens, reject unsupported action effects, and show rejected package diagnostics in Developer Mode | Network catalog, download, install/update, script execution/richer API injection, and runtime crash capture are still missing. |
| App Store | Prototype/Partial | `LZ_STORE` static catalog remains; local manifests from SD/appfs are scanned, listed as installed local apps, open a manifest detail shell, show storage quota usage, clear scoped app data on request, launch into the SDK 0.1 foreground shell, expose bounded foreground actions with scoped storage counters plus read-only `{time}`/`{battery}` tokens, enforce a 704-byte resident source/metadata runtime budget, reject unsupported action effects, and show rejected package diagnostics in Developer Mode | Network catalog, download, install/update, script execution/richer API injection, and runtime crash capture are still missing. |
| App Store | Prototype/Partial | `LZ_STORE` static catalog remains; local manifests from SD/appfs are scanned, listed as installed local apps, open a manifest detail shell, show storage quota usage, clear scoped app data on request, launch into the SDK 0.1 foreground shell, expose bounded foreground actions with scoped storage counters plus read-only `{time}`/`{battery}` tokens and permission-gated `notify:` feedback requests, reject unsupported action effects, and show rejected package diagnostics in Developer Mode | Network catalog, download, install/update, script execution/richer API injection, and runtime crash capture are still missing. |
| App Store | Prototype/Partial | `LZ_STORE` static catalog remains; local manifests from SD/appfs are scanned, listed as installed local apps, open a manifest detail shell, show storage quota usage, clear scoped app data on request, uninstall local apps with keep-data or delete-data choices, launch into the SDK 0.1 foreground shell, expose bounded foreground actions with scoped storage counters plus read-only `{time}`/`{battery}` tokens, reject unsupported action effects, and show rejected package diagnostics in Developer Mode | Network catalog, download, install/update, script execution/richer API injection, and runtime crash capture are still missing. |
| App Store | Prototype/Partial | `LZ_STORE` static catalog remains with prototype versions; local manifests from SD/appfs are scanned, listed as installed local apps, show catalog-version update chips when newer metadata exists, open a manifest detail shell, show storage quota usage, clear scoped app data on request, launch into the SDK 0.1 foreground shell, expose bounded foreground actions with scoped storage counters plus read-only `{time}`/`{battery}` tokens, reject unsupported action effects, and show rejected package diagnostics in Developer Mode | Network catalog, download, install/update, script execution/richer API injection, and runtime crash capture are still missing. |
| App Store | Prototype/Partial | `LZ_STORE` static catalog remains; local manifests from SD/appfs are scanned, listed as installed local apps, open a manifest detail shell, show storage quota usage, clear scoped app data on request, launch into the SDK 0.1 foreground shell, expose bounded foreground actions with scoped storage counters plus read-only `{time}`/`{battery}` tokens, capture bounded launch/action faults, reject unsupported action effects, and show rejected package diagnostics in Developer Mode | Network catalog, download, install/update, script execution/richer API injection, and full VM crash capture are still missing. |
| App Store | Prototype/Partial | Local manifests from SD/appfs are scanned, listed as installed local apps, open a manifest detail shell, show storage quota usage, clear scoped app data on request, uninstall local apps with keep-data or delete-data choices, launch into the SDK 0.1 foreground shell, expose bounded foreground actions with scoped storage counters plus read-only `{time}`/`{battery}` tokens and permission-gated `notify:` feedback requests, capture bounded launch/action faults, reject unsupported action effects, show rejected package diagnostics in Developer Mode, and render cached canonical network catalog entries as browse-only metadata rows with local update chips | Download/install/update, script execution/richer API injection, and full VM crash capture are still missing. |
| Terminal | Functional/Partial | interactive UI terminal behind Developer Mode; serial CLI always available over USB | Expand diagnostics once Developer Mode grows into a full power-user surface. |
| Files | Functional/Partial | read-only bounded filesystem browser rooted at mounted SD/local store or mounted FAT appfs; when both are present it starts at a Storage root picker | Add gated file actions later. |

Expand All @@ -117,16 +112,11 @@ Status labels:
| Local app scanner | Partial | `lz_store_scan_apps` scans `/sd/limitlezz/apps`, `/sd/apps`, `/appfs/apps`, simulator `<datadir>/apps`, and simulator `<datadir>/appfs/apps`; accepted apps appear in the paged Home launcher and App Store; rejected packages are exposed through Developer Mode diagnostics; simulator selftest covers appfs-only discovery, valid metadata, storage sandbox prep, quota usage, clear-data behavior, foreground launch metadata/actions, runtime memory-budget enforcement, storage counter persistence, read-only time/battery token gating, unsupported action-effect blocking, oversized entry blocking, and rejected unsafe packages | Add script execution, richer app lifecycle hooks, and broader user-facing data actions once memory profiling picks a runtime. |
| App permissions | Partial | Local manifests can declare allowlisted SDK namespaces (`display`, `input`, `storage`, mesh, time, battery, notifications, Wi-Fi); unknown permission names reject the package before Home/App Store; `storage` prepares a scoped package `data/` directory with a 64 KB launch-time quota guard, SDK action counters require both `input` and `storage`, and `{time}`/`{battery}` tokens require matching `system_time`/`battery` permission before launch | Implement least-privilege API injection when the runtime is selected. |
| Local app scanner | Partial | `lz_store_scan_apps` scans `/sd/limitlezz/apps`, `/sd/apps`, `/appfs/apps`, simulator `<datadir>/apps`, and simulator `<datadir>/appfs/apps`; accepted apps appear in the paged Home launcher and App Store; rejected packages are exposed through Developer Mode diagnostics; simulator selftest covers appfs-only discovery, valid metadata, storage sandbox prep, quota usage, clear-data behavior, foreground launch metadata/actions, storage counter persistence, bounded launch/action fault snapshots, read-only time/battery token gating, unsupported action-effect blocking, oversized entry blocking, and rejected unsafe packages | Add script execution, richer app lifecycle hooks, and broader user-facing data actions once memory profiling picks a runtime. |
| Network app catalog | Planned | Wi-Fi service notes; design spec | Fetch `index.json`, verify TLS/metadata, cache results. |
| Network app catalog | Partial | `docs/tdeck-app-catalog-schema.md`; bounded `limitlezz.app_catalog.v1` validator plus serial `app catalog status\|test` diagnostics | Fetch `index.json` over Wi-Fi, verify TLS/source metadata, cache results, and feed validated entries into App Store state. |
| Network app catalog | Planned/Partial | Wi-Fi service notes; design spec; Settings persists an App source selector with Official, Community, and Local only modes, and App Store reflects the selected source while keeping catalog examples hidden in local-only mode. | Fetch `index.json`, verify TLS/metadata, cache results. |
| Network app catalog | Planned/Partial | Wi-Fi service notes; bounded catalog JSON cache APIs | Fetch `index.json`, verify TLS/metadata, parse schema, and render cached results. |
| Network app catalog | Planned/Partial | Wi-Fi service notes; bounded T-Deck HTTP/HTTPS fetch transport foundation | Fetch `index.json` into the parser/cache flow, verify TLS/metadata, and render cached results. |
| Network app catalog | Partial | `docs/tdeck-network-app-catalog.md`, `docs/examples/app-catalog-index.json`, and `scripts/validate_app_catalog.py` define and CI-validate the first HTTPS `index.json` contract with SDK compatibility, permissions, package hash/size, and screenshots | Fetch `index.json` over Wi-Fi, verify TLS/metadata on-device, and cache results. |
| Network app catalog | Partial | `docs/tdeck-network-app-catalog.md`, `docs/examples/app-catalog-index.json`, and `scripts/validate_app_catalog.py` define and CI-validate `limitlezz.app.catalog.v1`; firmware validates the same canonical fields, loads typed cached entries, exposes serial `app catalog status\|test`, and feeds cached rows into App Store browsing/update metadata | Wire Wi-Fi fetch into source selection, verify TLS/source metadata, then add package download/install/update. |
| App download/install/update | Planned | App Store prototype only | SHA256 verify, extract, version updates, rollback failed installs. |
| App download/install/update | Planned/Partial | App Store prototype plus bounded package-file SHA256 helper | Wire the verifier into download/staging, then extract, version updates, and rollback failed installs. |
| Optional map app | Planned | Store data includes maps; maintainer notes prefer maps as optional | Keep maps out of the base firmware. |
| APRS/weather/BBS/scope/game apps | Planned/Prototype catalog entries | Static `LZ_STORE` rows | Implement as sandboxed apps once runtime exists. |
| APRS/weather/BBS/scope/game apps | Planned/Prototype sample apps | `examples/local-apps/` sample packages and canonical catalog example metadata | Implement richer sandboxed behavior once runtime APIs exist. |

## Security, Updates, And Feedback

Expand Down
5 changes: 2 additions & 3 deletions docs/tdeck-firmware-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,16 +364,15 @@ Goal: let users install and update apps from a repository.

Deliverables:

- Define catalog `index.json` schema: app id, name, version, author, description, icon id/color, permissions, download URL, SHA256, size, compatibility, screenshots if desired. Implemented: a bounded `limitlezz.app_catalog.v1` validator rejects unsafe IDs, unsupported permissions/SDK versions, non-HTTP package URLs, bad SHA256 values, oversize packages, and malformed optional screenshots; serial `app catalog status|test` exposes the result without requiring Wi-Fi.
- Define catalog `index.json` schema: app id, name, version, author, description, icon id/color, permissions, download URL, SHA256, size, compatibility, screenshots if desired. Implemented in `docs/tdeck-network-app-catalog.md` with `docs/examples/app-catalog-index.json`, `scripts/validate_app_catalog.py`, and a Firmware CI validation step.
- Define catalog `index.json` schema: app id, name, version, author, summary/description, icon id/color, permissions, package URL, SHA256, size, compatibility, and screenshots. Implemented in `docs/tdeck-network-app-catalog.md` as `limitlezz.app.catalog.v1`, with `docs/examples/app-catalog-index.json`, `scripts/validate_app_catalog.py`, and a Firmware CI validation step.
- Fetch catalog over Wi-Fi.
- Cache catalog for offline browsing. Initial implementation: bounded atomic
catalog JSON cache save/load/clear service APIs with native simulator coverage.
- Define catalog `index.json` schema: app id, name, version, author, description, icon id/color, permissions, download URL, SHA256, size, compatibility, screenshots if desired.
- Fetch catalog over Wi-Fi. Initial implementation: bounded T-Deck HTTP/HTTPS
catalog fetch transport gated on connected Wi-Fi, with native simulator stub
coverage for URL/buffer errors.
- Cache catalog for offline browsing.
- Cache catalog for offline browsing. Initial implementation: canonical cached catalog entries are parsed into typed firmware rows and rendered in App Store browsing/update metadata without fake install state.
- Download app zip/package.
- Verify SHA256 before install. Initial foundation: reusable package-file SHA256
hashing and expected-hash verification helpers with native simulator coverage.
Expand Down
5 changes: 3 additions & 2 deletions docs/tdeck-network-app-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ Each app entry must include:
- `hue`: tile hue hint, `-1` for neutral or `0..359`
- `package_url`: HTTPS URL for the app package
- `package_sha256`: lowercase 64-character SHA256 digest of the package
- `package_bytes`: positive package size in bytes
- `package_bytes`: positive package size in bytes, no more than `2 MB` for
the first firmware install path
- `compatibility`: object with `api_versions`, `targets`, and optional `min_os`
- `screenshots`: optional array of HTTPS screenshot metadata
- `screenshots`: optional array of up to `4` HTTPS screenshot metadata objects

The firmware should treat unknown fields as forward-compatible display metadata
only after the required fields pass validation. Required-field failures,
Expand Down
12 changes: 6 additions & 6 deletions scripts/tdeck_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,16 @@ def nostub_upload(project_dir: Path, env_name: str, port: str, baud: int, artifa
"--baud",
str(baud),
"--before",
"default-reset",
"default_reset",
"--after",
"hard-reset",
"hard_reset",
"--no-stub",
"write-flash",
"--flash-mode",
"write_flash",
"--flash_mode",
"dio",
"--flash-freq",
"--flash_freq",
"80m",
"--flash-size",
"--flash_size",
"16MB",
"0x0",
str(bootloader),
Expand Down
8 changes: 7 additions & 1 deletion scripts/validate_app_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"network_wifi",
}
TARGETS = {"tdeck", "sim"}
MAX_PACKAGE_BYTES = 2 * 1024 * 1024
MAX_SCREENSHOTS = 4

SAFE_ID = re.compile(r"^[A-Za-z0-9_.-]{1,23}$")
SAFE_VERSION = re.compile(r"^[0-9][0-9A-Za-z_.+-]{0,15}$")
Expand Down Expand Up @@ -119,6 +121,8 @@ def validate_screenshots(app: dict, path: str, errors: list[str]) -> None:
if not isinstance(screenshots, list):
add_error(errors, f"{path}.screenshots", "must be an array")
return
if len(screenshots) > MAX_SCREENSHOTS:
add_error(errors, f"{path}.screenshots", f"too many screenshots (max {MAX_SCREENSHOTS})")
for i, shot in enumerate(screenshots):
spath = f"{path}.screenshots[{i}]"
if not isinstance(shot, dict):
Expand Down Expand Up @@ -146,7 +150,7 @@ def validate_app(app: object, index: int, ids: set[str], errors: list[str]) -> N

app_id = require_string(app, "id", path, errors, 23)
if app_id:
if not SAFE_ID.match(app_id):
if not SAFE_ID.match(app_id) or app_id == "." or ".." in app_id:
add_error(errors, f"{path}.id", "unsafe id")
if app_id in ids:
add_error(errors, f"{path}.id", f"duplicate id {app_id!r}")
Expand Down Expand Up @@ -181,6 +185,8 @@ def validate_app(app: object, index: int, ids: set[str], errors: list[str]) -> N
package_size = require_int(app, "package_bytes", path, errors)
if package_size is not None and package_size <= 0:
add_error(errors, f"{path}.package_bytes", "must be positive")
if package_size is not None and package_size > MAX_PACKAGE_BYTES:
add_error(errors, f"{path}.package_bytes", f"must be <= {MAX_PACKAGE_BYTES}")

validate_compat(app, path, api_version, errors)
validate_screenshots(app, path, errors)
Expand Down
Loading