Skip to content

Scene permissions: apps declare, scenes add up, the frame enforces - #556

Merged
mariusandra merged 13 commits into
mainfrom
store-scene-asset-sandbox
Oct 17, 2026
Merged

mariusandra merged 13 commits into
mainfrom
store-scene-asset-sandbox

Conversation

@mariusandra

@mariusandra mariusandra commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Threat

A scene from someone else (the scene store, a URL, a repository) is anyone's code. Until now:

  • its JS apps had the run of the frame's assets folder;
  • data/localImage could name any folder on disk;
  • nothing told the person installing it.

The first version of this PR confined store-origin scenes by provenance (origin.storeSceneId). That bit is hard to keep true across copies, forks and imports, so the frame no longer reads it anywhere. Permissions are now part of the content, declared by apps.

Apps ask, scenes add up

Each app's config.json has a permissions list with four names. A scene asks for exactly what its apps ask for.

permission label who
assets reads assets folder JS apps calling listAssets, readAsset, loadAssetImage, a stream opened r, ...
assets-write writes assets folder JS apps calling writeAsset, appendAsset, deleteAsset, a stream opened w/a
files reads all files built-in Local Image
shell runs shell commands built-in Chromium screenshot and RTSP snapshot
  • API keys show too: each settings namespace an app reads appears as "uses OpenAI key" (key:openAI).
  • Code nodes ask for nothing. They have no asset or process API. Only legacy Nim code can still flag shell.
  • Built-in apps declare theirs in their own config.json. BUILTIN_APP_ACCESS in frontend/src/utils/sceneAccess.ts mirrors them, with a test against every config.json.
  • JS apps get their list automatically. It follows the app's code on every save: frame saves on both planes, template save and publish, and the wasm preview payload. There is no hand override: a list written by hand is replaced on the next save.
  • The frame enforces the list per JS app (JsAssetGrant in js_runtime/app_runtime.nim, read once from config.json, like settings gates getSetting):
    • none of assets/files/assets-write, or no list at all: the scene's own <assets>/scenes/<sceneId>/, read and write;
    • assets or files: read the frame's assets folder, no writes;
    • assets-write: read and write it.
  • Unchanged: fetchImage({saveAsset}) is frame caching and stays in saved/. js.assetSandbox: "scene" still confines every app.

Where it shows

  • Tags on every surface: frame scene rows, the scene drawer, template and store tiles, the cloud scene card and scene page.
  • Scene settings: a "Permissions" block lists each flag with the app(s) that ask for it, and warns when a hand-set list misses something the code calls.
  • Store: risk_flags are computed at publish with the same detector, keys included.
  • Installs: installing is the approval. "Add scene from URL" asks first when a scene has any flag.
  • Updates: the update dialog reads the new version and says what it gains, for the clicked scene and for "Update all scenes". Its button turns red for shell, files or writes, and the update buttons show "Checking…" meanwhile.
  • Lint: a permissions list may only use the four names.

docs/scene-access-fixtures.json is the corpus: scenes, sets, changes, and stamp cases that pin the exact config.json output.

Existing scenes keep working

  • Self-hosted backend: migration 5e2a9c71d4b8 writes every stored JS app's list from its code: frames, their deploy baseline, and templates. It uses a frozen Python copy of the detection that replays the shared stamp corpus. Lists only appear where missing, and hand-set lists and unparseable files are left alone. A frame gets the lists with the deploy that installs the release that reads them.
  • Cloud: store versions are immutable, so nothing is rewritten.
    • The device push stamps missing lists for firmware from APP_PERMISSIONS_FROM (2026.10.16).
    • The hub's firmware-change resync now also fires across that version, so each frame gets its lists, on the scene it is showing, the moment it starts enforcing them. Nothing to run.
    • The scenes.json route and zip download stamp always.
  • Edge case: a frame that upgrades on its own, outside a backend deploy, enforces before its next deploy brings the lists. Only custom JS apps that call asset functions notice; no repo app does.

Shell apps and the LAN deny drop provenance too

  • Shell apps: a store-stamped scene no longer needs the on-panel shellApps ceremony to run the two process-spawning apps. Their shell permission is shown, and installing approves it.
    • URL checks on what the child process opens stay.
    • Cloud-managed frames still refuse both apps.
    • shellApps now only lets Chromium run unsandboxed as root.
  • LAN deny: armed by the installer (a cloud-managed frame, or cloud-pushed scenes), on Linux and ESP32. A resident store scene no longer arms it.

ESP32 size

The frame side, against main, is +172/−289 lines. The scene-level code (settings.assetAccess, asset_scope.nim, the Local Image confinement, the saveAsset redirect) is gone.

Measured on the generated ESP32 C compiled at -Os on the host:

  • app_runtime: +868 B text and +112 B data. The write check is a single condition in resolveAssetPath, so no new exits repeat ORC cleanup.
  • scene_guard (about 500 B) drops out of the ESP32 build.
  • fos_scenes.c loses its per-load strstr.

Also

Store page: category headers text-lg → text-2xl, scene names in cards text-sm → text-base.

After deploy

Run pnpm --filter @frameos-cloud/auth-web scene:backfill-risk-flags --apply to recompute existing store versions' tags, keys included. It is a dry run without --apply.

Not in this PR

  • Scenes on self-hosted frames reach the LAN freely. Next step in docs/security-todo.md: a declared lan permission.
  • Which API keys a store scene is granted is unchanged (provenance-based); this PR only shows them.

Tests

  • Nim:
    • test_js_app_runtime: per-app grants, no list, non-list, and the frame-folder tests asking explicitly;
    • every JS-runtime test and the repo apps' own-test harness (19 files);
    • spawn guard, chromium, rstp, Local Image, scenes persistence, interpreter state, single-scene host;
    • nim check for Linux, ESP32 and wasm.
  • Backend:
    • the full suite (1919 passed);
    • test_app_permissions_migration.py (corpus parity, streams, inline and non-JS apps, frames + baseline + templates);
    • the full alembic chain on a fresh SQLite.
  • Cloud:
    • pnpm verify (28/28);
    • auth-web integration (520/520);
    • frame-hub integration (66/66, including the resync across 2026.10.16);
    • ledger integration (52/52).
  • Frontend: tsc, kea-typegen check, prettier, and the shared-spa scene-access (62) and update tests.

🤖 Generated with Claude Code

…ault

A scene installed from the scene store is anyone's code, whichever control
plane installed it. Until now it read and wrote the whole of /srv/assets
through the JS asset calls (fonts, other scenes' files, .frameos/scene_images)
and `data/localImage`'s `path` named any folder on disk. The per-scene asset
sandbox existed but was opt-in (`js.assetSandbox: "scene"`).

One module, frameos/asset_scope.nim, now answers "which folder is this
scene's" for every consumer of a scene-supplied path: the JS asset API,
`saveAsset`, and the localImage app. The rule keys on the same
`storeOrigin` bit the shell-apps gate uses:

  "store" (new default): store-origin scenes get <assets>/scenes/<sceneId>/
          and nothing else; the owner's scenes share the frame's folder
  "scene": every scene its own subtree
  "frame": nothing confined (the old behavior)

The containment check (lexical, then against the real path so a planted
symlink is not a way out) moved there unchanged. For a confined scene,
localImage accepts an empty or relative path inside its subtree, or an
absolute one already pointing into it, and paints a refusal for anything
else. The wasm preview confines nothing under "store": it is the author's
own machine. An unknown value falls back to "store", never to open.

Tests: test_asset_scope (new), test_js_app_runtime (store vs owner under
the default, "frame" switches it off), localImage test_app (refusal, the
three accepted path shapes, the owner's scene unchanged),
test_config_helpers (new default). Docs: scene guide (regenerated copies),
js-apps doc, cloud-frames.md posture; the security-todo item is deleted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

ESP32 firmware size

Built from 0411dbb by this run; compared with the latest release v2026.10.15.

Image This PR v2026.10.15 Δ
esp32-s3 app (OTA image) 3,141,376
87.2% of 3520K slot, 452 KB free
3,138,272 +3,104 (+0.1%)
esp32-s3 merged flash image 3,206,912 3,203,808 +3,104 (+0.1%)
esp32-s3-32mb app (OTA image) 3,141,360
76.1% of 4032K slot, 964 KB free
3,138,272 +3,088 (+0.1%)
esp32-s3-32mb merged flash image 3,272,432 3,269,344 +3,088 (+0.1%)

Breakdown by subsystem — esp32-s3

Subsystem This PR v2026.10.15 Δ
FrameOS core (Nim) 479,344 477,042 +2,302 (+0.5%)
QuickJS 360,413 360,373 +40 (+0.0%)
pixie 304,418 304,354 +64 (+0.0%)
Wi-Fi stack 295,304 295,304 ±0
Nim stdlib 251,555 251,436 +119 (+0.0%)
FrameOS apps (Nim) 190,448 190,356 +92 (+0.0%)
ESP-IDF misc 181,371 181,564 -193 (-0.1%)
Embedded font 146,362 146,362 ±0
String pool (attributed to efuse) 135,840 136,134 -294 (-0.2%)
fos_* firmware shell (C) 133,174 132,233 +941 (+0.7%)
mbedTLS + certificates 132,439 132,439 ±0
lwIP / HTTP / WebSocket 126,792 126,800 -8 (-0.0%)
libc / libm / newlib 116,263 116,263 ±0
Storage (SPIFFS / FatFS / SD / NVS) 94,076 94,080 -4 (-0.0%)
Nim packages (chrono, zippy, chroma, qrgen, ...) 71,845 71,849 -4 (-0.0%)
Display drivers (C) 64,958 64,946 +12 (+0.0%)
Crypto (monocypher) 35,141 35,141 ±0
Total mapped flash 3,119,743 3,116,676 +3,067 (+0.1%)
Inside each subsystem

FrameOS core (Nim) — 479,344

Part This PR v2026.10.15 Δ
frameos/js_runtime 116,841 115,444 +1,397 (+1.2%)
frameos/utils 112,365 112,401 -36 (-0.0%)
frameos/interpreter.nim 100,260 100,264 -4 (-0.0%)
embedded_main.nim 19,252 19,252 ±0
embedded_runtime.nim 17,830 17,834 -4 (-0.0%)
lib/tz.nim 16,722 16,722 ±0
frameos_nim_glue.c.obj 10,679 10,635 +44 (+0.4%)
frameos/planner.nim 9,689 9,681 +8 (+0.1%)
frameos/event_loop.nim 8,412 8,412 ±0
frameos/types.nim 7,909 7,909 ±0
frameos/values.nim 7,699 7,699 ±0
frameos/single_scene_host.nim 6,687 6,712 -25 (-0.4%)
frameos/apps.nim 6,585 6,585 ±0
frameos/spool.nim 5,787 5,787 ±0
frameos/input_state.nim 4,947 3,766 +1,181 (+31.4%)
embedded_scene.nim 4,945 4,945 ±0
frameos/events_gen.nim 3,948 3,948 ±0
frameos/app_config.nim 2,951 2,935 +16 (+0.5%)
frameos/reboot_reason.nim 2,648 2,648 ±0
frameos/node_config.nim 2,618 2,618 ±0
frameos/app_capabilities.nim 2,489 2,489 ±0
frameos/events.nim 2,410 2,410 ±0
fos_netguard.c.obj 2,179 2,179 ±0
frameos/refresh_interval.nim 1,635 1,639 -4 (-0.2%)
frameos/runtime_diagnostics.nim 767 767 ±0
frameos/event_log.nim 415 431 -16 (-3.7%)
fos_version.c.obj 328 328 ±0
frameos/cloud — 255 -255 (-100.0%)
frameos/channels.nim 152 152 ±0
frameos/ids.nim 137 137 ±0
frameos/hal 44 44 ±0
frameos/retired_apps.nim 14 14 ±0

QuickJS — 360,413

Part This PR v2026.10.15 Δ
quickjs.c.obj 287,886 287,846 +40 (+0.0%)
libunicode.c.obj 50,178 50,178 ±0
libregexp.c.obj 13,999 13,999 ±0
dtoa.c.obj 5,685 5,685 ±0
cutils.c.obj 2,141 2,141 ±0
fos_qjs_glue.c.obj 449 449 ±0
fos_quickjs_tz.c.obj 75 75 ±0

pixie — 304,418

Part This PR v2026.10.15 Δ
pixie/fontformats/opentype.nim 55,460 55,460 ±0
pixie/fileformats/webp.nim 37,493 37,493 ±0
pixie/fileformats/svg.nim 36,983 36,983 ±0
pixie/paths.nim 32,875 32,875 ±0
pixie/fileformats/jpeg.nim 29,440 29,436 +4 (+0.0%)
pixie/fileformats/png.nim 22,101 22,101 ±0
pixie/images.nim 15,779 15,779 ±0
pixie/fonts.nim 10,995 10,995 ±0
pixie/paints.nim 9,869 9,869 ±0
pixie/fileformats/bmp.nim 8,278 8,258 +20 (+0.2%)
pixie/common.nim 7,417 7,413 +4 (+0.1%)
pixie/blends.nim 6,516 6,516 ±0
pixie/fileformats/gif.nim 5,961 5,965 -4 (-0.1%)
pixie/inflatestream.nim 5,233 5,233 ±0
pixie.nim 5,224 5,224 ±0
pixie/fileformats/webp_vp8_tables.nim 3,881 3,881 ±0
pixie/fileformats/ppm.nim 3,725 3,713 +12 (+0.3%)
pixie/contexts.nim 3,058 3,054 +4 (+0.1%)
pixie/fileformats/qoi.nim 2,726 2,702 +24 (+0.9%)
pixie/internal.nim 754 754 ±0
pixie/rgb565.nim 385 385 ±0
pixie/fontformats/svgfont.nim 215 215 ±0
pixie/decodebudget.nim 50 50 ±0

Wi-Fi stack — 295,304

Part This PR v2026.10.15 Δ
libnet80211.a 136,168 136,168 ±0
libpp.a 62,154 62,154 ±0
libwpa_supplicant.a 54,904 54,904 ±0
libphy.a 34,176 34,176 ±0
libesp_wifi.a 4,780 4,780 ±0
libesp_phy.a 2,541 2,541 ±0
libesp_coex.a 291 291 ±0
libcore.a 287 287 ±0
libespnow.a 3 3 ±0

Nim stdlib — 251,555

Part This PR v2026.10.15 Δ
pure/collections/tables.nim 72,312 72,312 ±0
system.nim 48,485 48,457 +28 (+0.1%)
pure/unicode.nim 17,405 17,405 ±0
pure/times.nim 16,888 16,888 ±0
pure/json.nim 13,726 13,722 +4 (+0.0%)
std/private/dragonbox.nim 10,351 10,351 ±0
pure/strutils.nim 8,047 8,047 ±0
pure/parsexml.nim 6,003 6,011 -8 (-0.1%)
pure/algorithm.nim 5,372 5,372 ±0
pure/base64.nim 5,290 5,290 ±0
pure/hashes.nim 4,181 4,181 ±0
pure/xmlparser.nim 3,555 3,555 ±0
pure/parsejson.nim 3,553 3,553 ±0
std/formatfloat.nim 3,504 3,504 ±0
pure/uri.nim 3,225 3,225 ±0
pure/streams.nim 2,820 2,820 ±0
pure/strformat.nim 2,546 2,546 ±0
std/syncio.nim 2,355 2,355 ±0
pure/collections/sets.nim 2,075 2,075 ±0
pure/xmltree.nim 2,047 2,047 ±0
pure/lexbase.nim 1,801 1,801 ±0
pure/parseutils.nim 1,750 1,750 ±0
system/exceptions.nim 1,598 1,598 ±0
pure/strtabs.nim 1,572 1,572 ±0
pure/pathnorm.nim 1,336 1,336 ±0
pure/options.nim 1,016 1,016 ±0
std/private/ospaths2.nim 994 994 ±0
pure/collections/lists.nim 979 979 ±0
pure/math.nim 841 841 ±0
pure/collections/deques.nim 822 822 ±0
pure/random.nim 696 696 ±0
std/private/digitsutils.nim 651 651 ±0
std/oserrors.nim 633 633 ±0
std/private/osdirs.nim 572 572 ±0
system/dollars.nim 529 529 ±0
std/monotimes.nim 501 406 +95 (+23.4%)
std/private/oscommon.nim 329 329 ±0
std/envvars.nim 250 250 ±0
std/private/decode_helpers.nim 240 240 ±0
std/private/ossymlinks.nim 187 187 ±0
pure/bitops.nim 162 162 ±0
pure/os.nim 99 99 ±0
std/private/osfiles.nim 96 96 ±0
std/assertions.nim 87 87 ±0
core/macros.nim 65 65 ±0
system/ctypes.nim 9 9 ±0

FrameOS apps (Nim) — 190,448

Part This PR v2026.10.15 Δ
apps/data/icalJson 50,737 50,733 +4 (+0.0%)
apps/render/calendar 30,894 30,874 +20 (+0.1%)
apps/data/localImage 10,432 10,432 ±0
apps/render/split 9,736 9,772 -36 (-0.4%)
apps/data/eventsToAgenda 9,068 9,068 ±0
apps/render/zoomPan 8,676 8,648 +28 (+0.3%)
apps/apps.nim 7,251 7,247 +4 (+0.1%)
apps/render/text 5,491 5,455 +36 (+0.7%)
apps/data/xmlToJson 5,428 5,428 ±0
apps/render/svg 5,244 5,192 +52 (+1.0%)
apps/render/image 4,724 4,708 +16 (+0.3%)
apps/data/rotateImage 3,465 3,465 ±0
apps/logic/setAsState 3,453 3,453 ±0
apps/data/downloadImage 3,317 3,325 -8 (-0.2%)
apps/render/gradient 3,217 3,229 -12 (-0.4%)
apps/data/qr 3,118 3,122 -4 (-0.1%)
apps/data/downloadUrl 3,053 3,053 ±0
apps/render/color 2,840 2,824 +16 (+0.6%)
apps/data/newImage 2,668 2,668 ±0
apps/data/resizeImage 2,558 2,558 ±0
apps/data/log 2,299 2,299 ±0
apps/render/opacity 2,288 2,296 -8 (-0.3%)
apps/data/prettyJson 2,225 2,225 ±0
apps/data/clock 1,871 1,875 -4 (-0.2%)
apps/data/parseJson 1,848 1,856 -8 (-0.4%)
apps/logic/nextSleepDuration 1,663 1,663 ±0
apps/logic/ifElse 1,494 1,494 ±0
apps/logic/breakIfRendering 1,390 1,394 -4 (-0.3%)

ESP-IDF misc — 181,371

Part This PR v2026.10.15 Δ
libesp_hw_support.a 37,640 37,632 +8 (+0.0%)
libhal.a 21,865 21,865 ±0
libfreertos.a 17,240 17,240 ±0
libesp_system.a 13,736 13,780 -44 (-0.3%)
libesp_driver_uart.a 11,627 11,627 ±0
libheap.a 8,660 8,660 ±0
libesp_driver_i2c.a 8,062 8,062 ±0
libesp_driver_spi.a 7,756 7,756 ±0
libconsole.a 5,733 5,733 ±0
libjson.a 4,940 4,940 ±0
libesp_driver_gpio.a 3,878 3,878 ±0
libbootloader_support.a 3,813 3,813 ±0
libxtensa.a 3,691 3,691 ±0
libesp_driver_usb_serial_jtag.a 3,408 3,408 ±0
libesp_ringbuf.a 3,322 3,322 ±0
libesp_event.a 3,253 3,253 ±0
libesp_driver_ledc.a 3,054 3,054 ±0
libesp_mm.a 2,451 2,451 ±0
libapp_update.a 2,414 2,414 ±0
libesp_timer.a 2,337 2,337 ±0
libesp_psram.a 2,172 2,329 -157 (-6.7%)
libsoc.a 2,316 2,316 ±0
libesp_adc.a 2,285 2,285 ±0
libpthread.a 1,397 1,397 ±0
liblog.a 1,375 1,375 ±0
libesp_security.a 1,050 1,050 ±0
libesp_rom.a 902 902 ±0
libesp_app_format.a 507 507 ±0
libxt_hal.a 437 437 ±0
libesp_common.a 26 26 ±0
libesp_pm.a 24 24 ±0

Embedded font — 146,362

Part This PR v2026.10.15 Δ
assets/fonts.nim 146,362 146,362 ±0

String pool (attributed to efuse) — 135,840

Part This PR v2026.10.15 Δ
esp_efuse_utility.c.obj 134,794 135,088 -294 (-0.2%)
esp_efuse_rtc_calib.c.obj 566 566 ±0
esp_efuse_table.c.obj 268 268 ±0
esp_efuse_api.c.obj 151 151 ±0
esp_efuse_startup.c.obj 61 61 ±0

fos_ firmware shell (C)* — 133,174

Part This PR v2026.10.15 Δ
fos_http.c.obj 22,185 22,185 ±0
fos_cloud.c.obj 19,185 19,258 -73 (-0.4%)
fos_console.c.obj 13,950 13,950 ±0
fos_client.c.obj 12,350 12,266 +84 (+0.7%)
fos_settings.c.obj 7,618 7,618 ±0
fos_scenes.c.obj 7,515 7,578 -63 (-0.8%)
fos_ota.c.obj 5,467 5,467 ±0
fos_cloud_contract.c.obj 4,744 4,744 ±0
fos_config.c.obj 4,619 4,619 ±0
fos_wifi.c.obj 3,189 3,189 ±0
fos_assets.c.obj 3,124 3,124 ±0
fos_buttons.c.obj 2,772 2,493 +279 (+11.2%)
fos_sd_probe.c.obj 2,753 2,753 ±0
fos_schedule.c.obj 2,602 2,602 ±0
fos_assets_sd.c.obj 2,484 2,484 ±0
fos_events.c.obj 2,320 2,320 ±0
fos_status_screen.c.obj 2,049 2,049 ±0
main.c.obj 1,703 1,699 +4 (+0.2%)
fos_config_parse.c.obj 1,467 1,467 ±0
fos_url_guard.c.obj 1,397 1,397 ±0
fos_tz.c.obj 1,294 1,294 ±0
fos_buzzer.c.obj 1,162 901 +261 (+29.0%)
fos_battery.c.obj 1,140 1,140 ±0
fos_minisig.c.obj 1,140 1,140 ±0
fos_backend_http.c.obj 1,072 1,072 ±0
fos_commands.c.obj 940 940 ±0
fos_scene_state.c.obj 650 650 ±0
fos_framebuffer.c.obj 483 483 ±0
fos_button_timing.c.obj 434 — +434
fos_board.c.obj 334 334 ±0
fos_assets_path.c.obj 259 259 ±0
fos_power.c.obj 241 241 ±0
fos_battery_filter.c.obj 214 214 ±0
fos_wake.c.obj 199 184 +15 (+8.2%)
fos_json_guard.c.obj 119 119 ±0

mbedTLS + certificates — 132,439

Part This PR v2026.10.15 Δ
libmbedcrypto.a 71,295 71,291 +4 (+0.0%)
libmbedtls.a 45,799 45,799 ±0
libmbedx509.a 7,088 7,092 -4 (-0.1%)
libesp-tls.a 6,705 6,705 ±0
libesp_https_server.a 1,552 1,552 ±0

lwIP / HTTP / WebSocket — 126,792

Part This PR v2026.10.15 Δ
liblwip.a 78,437 78,445 -8 (-0.0%)
libhttp_parser.a 10,350 10,350 ±0
libesp_http_server.a 10,020 10,020 ±0
libtcp_transport.a 8,245 8,245 ±0
libespressif__esp_websocket_client.a 6,822 6,822 ±0
libesp_http_client.a 6,666 6,666 ±0
libesp_netif.a 6,252 6,252 ±0

libc / libm / newlib — 116,263

Part This PR v2026.10.15 Δ
libc.a 76,471 76,471 ±0
libm.a 31,563 31,563 ±0
libnewlib.a 6,609 6,609 ±0
libstdc++.a 1,476 1,476 ±0
libgcc.a 98 98 ±0
libcxx.a 46 46 ±0

Storage (SPIFFS / FatFS / SD / NVS) — 94,076

Part This PR v2026.10.15 Δ
libspiffs.a 20,380 20,380 ±0
libfatfs.a 20,133 20,133 ±0
libspi_flash.a 14,168 14,168 ±0
libnvs_flash.a 13,681 13,685 -4 (-0.0%)
libsdmmc.a 13,013 13,013 ±0
libvfs.a 6,196 6,196 ±0
libesp_driver_sdspi.a 3,866 3,866 ±0
libesp_partition.a 1,956 1,956 ±0
libesp_vfs_console.a 678 678 ±0
libnvs_sec_provider.a 5 5 ±0

Nim packages (chrono, zippy, chroma, qrgen, ...) — 71,845

Part This PR v2026.10.15 Δ
QRgen 3.1.0 22,056 22,060 -4 (-0.0%)
chrono 0.3.1 15,586 15,586 ±0
zippy 0.10.19 15,200 15,200 ±0
chroma 1.0.0 14,714 14,714 ±0
checksums 0.2.1 3,371 3,371 ±0
flatty 0.3.4 533 533 ±0
jsony 1.1.5 272 272 ±0
bumpy 1.1.3 113 113 ±0

Display drivers (C) — 64,958

Part This PR v2026.10.15 Δ
(everything else) 21,278 21,282 -4 (-0.0%)
frameos_panel_table.c.obj 6,685 6,685 ±0
EPD_13in3e.c.obj 1,796 1,796 ±0
EPD_3in7.c.obj 1,564 1,564 ±0
EPD_5in79.c.obj 1,481 1,481 ±0
DEV_Debug.c.obj 1,325 1,317 +8 (+0.6%)
EPD_2in7.c.obj 1,292 1,292 ±0
EPD_4in2.c.obj 1,262 1,262 ±0
EPD_7in3e.c.obj 1,208 1,208 ±0
EPD_4in2_V2.c.obj 1,184 1,184 ±0
DEV_Config_esp.c.obj 1,180 1,180 ±0
EPD_4in26.c.obj 1,123 1,123 ±0
29 smaller rows 23,580 23,572 +8 (+0.0%)

Crypto (monocypher) — 35,141

Part This PR v2026.10.15 Δ
monocypher.c.obj 31,916 31,916 ±0
monocypher-ed25519.c.obj 3,225 3,225 ±0
Biggest movers (per object)
Object Δ This PR Baseline
frameos/input_state.nim +4,947 4,947 —
pkgs/QRgen-3.1.0/QRgen/renderer.nim -3,780 — 3,780
frameos/js_runtime/app_runtime.nim +1,441 74,076 72,635
esp_efuse_utility.c.obj -294 134,794 135,088
fos_client.c.obj +84 12,350 12,266
fos_cloud.c.obj -73 19,185 19,258
fos_scenes.c.obj -63 7,515 7,578
frameos/js_runtime/runtime.nim -48 31,638 31,686
frameos_nim_glue.c.obj +44 10,679 10,635
quickjs.c.obj +40 287,886 287,846
apps/render/split/app_loader.nim -32 4,670 4,702
nim/lib/system.nim +28 48,485 48,457
frameos/single_scene_host.nim -25 6,687 6,712
pkgs/pixie-6.1.0/pixie/fileformats/bmp.nim +20 8,278 8,258
apps/render/calendar/app_loader.nim +20 7,077 7,057
Largest 30 objects
Object Subsystem This PR v2026.10.15 Δ
quickjs.c.obj QuickJS 287,886 287,846 +40 (+0.0%)
assets/fonts.nim Embedded font 146,362 146,362 ±0
esp_efuse_utility.c.obj String pool (attributed to efuse) 134,794 135,088 -294 (-0.2%)
frameos/interpreter.nim FrameOS core (Nim) 100,260 100,264 -4 (-0.0%)
frameos/js_runtime/app_runtime.nim FrameOS core (Nim) 74,076 72,635 +1,441 (+2.0%)
nim/lib/pure/collections/tables.nim Nim stdlib 72,312 72,312 ±0
pkgs/pixie-6.1.0/pixie/fontformats/opentype.nim pixie 55,460 55,460 ±0
libunicode.c.obj QuickJS 50,178 50,178 ±0
nim/lib/system.nim Nim stdlib 48,485 48,457 +28 (+0.1%)
apps/data/icalJson/ical.nim FrameOS apps (Nim) 45,020 45,016 +4 (+0.0%)
pkgs/pixie-6.1.0/pixie/fileformats/webp.nim pixie 37,493 37,493 ±0
pkgs/pixie-6.1.0/pixie/fileformats/svg.nim pixie 36,983 36,983 ±0
frameos/utils/image.nim FrameOS core (Nim) 35,942 35,942 ±0
pkgs/pixie-6.1.0/pixie/paths.nim pixie 32,875 32,875 ±0
monocypher.c.obj Crypto (monocypher) 31,916 31,916 ±0
frameos/js_runtime/runtime.nim FrameOS core (Nim) 31,638 31,686 -48 (-0.2%)
pkgs/pixie-6.1.0/pixie/fileformats/jpeg.nim pixie 29,440 29,436 +4 (+0.0%)
apps/render/calendar/app.nim FrameOS apps (Nim) 23,817 23,817 ±0
fos_http.c.obj fos_* firmware shell (C) 22,185 22,185 ±0
pkgs/pixie-6.1.0/pixie/fileformats/png.nim pixie 22,101 22,101 ±0
embedded_main.nim FrameOS core (Nim) 19,252 19,252 ±0
fos_cloud.c.obj fos_* firmware shell (C) 19,185 19,258 -73 (-0.4%)
frameos/utils/status_screen.nim FrameOS core (Nim) 17,939 17,943 -4 (-0.0%)
x509_crt_bundle.S.obj mbedTLS + certificates 17,928 17,928 ±0
embedded_runtime.nim FrameOS core (Nim) 17,830 17,834 -4 (-0.0%)
nim/lib/pure/unicode.nim Nim stdlib 17,405 17,405 ±0
frameos/utils/asset_colors.nim FrameOS core (Nim) 17,333 17,333 ±0
nim/lib/pure/times.nim Nim stdlib 16,888 16,888 ±0
lib/tz.nim FrameOS core (Nim) 16,722 16,722 ±0
pkgs/pixie-6.1.0/pixie/images.nim pixie 15,779 15,779 ±0

Flash = .text + .rodata from the linker map via esp_idf_size; "Inside each subsystem" splits every bucket one level down (the Nim core by directory, apps by app, packages by package, ESP-IDF by archive) over all objects, not just the largest. "String pool (attributed to efuse)" is the linker's merged string-literal pool for the whole image, not efuse code — see docs/esp32-image-size.md.

mariusandra and others added 4 commits October 17, 2026 02:20
Store provenance (`storeOrigin`) is hard to keep true across copies, forks
and imports, so it no longer decides what a scene may read. Instead:

- `settings.assetAccess` on the scene: "auto" (default), "cache-home-only"
  (the scene's own <assets>/scenes/<id>/ for asset calls, saveAsset and
  Local Image), "all-assets" (Local Image held inside the assets folder) or
  "all-files". The frame enforces the explicit values through
  asset_scope.nim; "auto" runs as before. `js.assetSandbox` goes back to
  "frame" (default) / "scene", the frame owner's ceiling.
- One detector, frontend/src/utils/sceneAccess.ts, reads what a scene
  reaches for from its JSON (asset calls, Local Image paths, shell apps) and
  gives the flags "runs shell commands", "reads assets folder", "reads all
  files". The SPA shows them on scene rows, the scene drawer, template and
  store tiles, and asks before a URL install; the cloud store computes
  risk_flags at publish with the same function (shell detection moved there
  too), and its scene lint checks the setting. Corpus:
  docs/scene-access-fixtures.json.
- Scene settings gain "Access to assets", whose Auto option says what was
  detected.
- A non-string settings.assetAccess no longer fails the scenes.json parse.

The shell-apps gate (spawn_guard.nim) still keys on storeOrigin, unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scene:backfill-risk-flags recomputes store_scene_versions.risk_flags (and
the latest version's copy on store_scenes) from each version's own scenes,
so versions published before the assets/files flags show them. Dry run by
default. Builtin apps catalog: the Local Image source it embeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariusandra mariusandra changed the title Asset sandbox: confine store-origin scenes to their own folder by default Scene asset access: a per-scene setting, flagged wherever the scene shows up Oct 17, 2026
FrameOS Bot and others added 6 commits October 17, 2026 00:34
A store stamp (origin.storeSceneId) is not something the frame can keep
true across copies, forks and imports, so the frame no longer reads it:

- spawn_guard.nim: a store-stamped scene no longer needs the on-panel
  "shellApps" ceremony to run chromiumScreenshot / rstpSnapshot. A scene
  that uses either app says so in its own JSON, every surface shows "runs
  shell commands", and installing it is the approval. The URL checks on what
  the child process opens stay, for every scene; cloud-managed frames still
  refuse both apps. The shellApps scope now only lets Chromium run
  unsandboxed where FrameOS runs as root.
- The private-network deny is armed by the installer (cloud-managed frame,
  cloud-pushed scenes), no longer by a store scene being resident, on Linux
  (hub_client.nim) and ESP32 (fos_cloud.c, fos_scenes.c).
- storeOrigin, storeOriginScenesResident, scenePayloadHasStoreOrigin and
  sceneOriginIsStore are gone. docs/security-todo.md gains the next step for
  the LAN: a content-based signal like the asset flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The asset flags split read from write: "writes assets folder"
  (assets-write) for writeAsset, appendAsset, deleteAsset and
  openAssetStream opened to write. Writes only ever land in the assets
  folder and Local Image only reads, so "reads all files" stays as it was;
  "reads assets folder" no longer hides writes. A cache-home-only scene
  shows neither.
- The "Update to latest" dialog reads the new version first (the store's
  scenes.json on the cloud, the template's scenes on the backend, catalog
  flags as the fallback, 3 s cap) and says what it gains, for the clicked
  scene and for "Update all scenes"; the button turns red when the clicked
  scene gains shell, files or writes. The update buttons say "Checking…"
  meanwhile.
- sceneAccessChanges / describeGainedAccess in sceneAccess.ts and
  sceneUpdateAccessNotice in sceneUpdateAccess.ts, with a `changes` section
  in docs/scene-access-fixtures.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…el code goes

What the frame enforces is what each app asks for in its config.json
`permissions`, the way `settings` already gates getSetting:

- JsAssetGrant (one byte per JS app runtime, parsed once in
  initDynamicJsApp): no "assets"/"files"/"assets-write" keeps the app's asset
  calls in the scene's own <assets>/scenes/<sceneId>/; "assets" or "files"
  reads the frame's folder and cannot change it; "assets-write" reads and
  writes it. No `permissions` key (apps from before 2026.10.16) runs as
  before. The write check is one condition in resolveAssetPath, which every
  caller already handles, so jsAssets gains no exits (each exit repeats the
  cleanup of all its live strings).
- Built-in Nim apps declare theirs statically (Local Image "files",
  chromiumScreenshot and rstpSnapshot "shell"); they are our code and are not
  checked.
- Gone: settings.assetAccess and its parse hook, asset_scope.nim, the Local
  Image confinement and the saveAsset redirect. localImage, apps.nim,
  config.nim, types.nim and interpreter.nim are back to main apart from the
  storeOrigin removal.

ESP32 cost, measured on the generated C (host -Os object): app_runtime
+868 B text / +112 B data against main; scene_guard (~500 B) drops out of
the ESP32 build, and fos_scenes.c loses its per-load strstr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore fonts

Permissions belong to apps: each app's config.json `permissions` (shell,
files, assets, assets-write), and a scene shows the union, plus the API keys
its apps read ("uses OpenAI key", `key:<namespace>`).

- Editor: sceneAccess.ts reads apps instead of scenes. Built-in apps declare
  statically (Local Image files, the two shell apps shell, checked against
  their config.json). A JS app's list follows its code on every save
  (withAppPermissions in sanitizeScene, the cloud scene saves, template
  save/publish and the wasm preview payload) unless it says
  "permissionsAuto": false. The scene settings panel lists what each app asks
  for; tags, the store, the URL-install dialog and the update dialog include
  the key flags. The scene-level settings.assetAccess is gone.
- Frame: asking for nothing gets nothing. An app with no list keeps its asset
  calls in the scene's own folder (JsAssetGrant: agUndeclared removed; the
  runtime constructor defaults to it too).
- Self-hosted backend: migration 5e2a9c71d4b8 writes every stored JS app's
  list from its code (frames, their deploy baseline, templates), with a
  frozen Python copy of the detection that replays the shared stamp corpus.
- Cloud: store versions stay immutable; the push stamps missing lists for a
  firmware from APP_PERMISSIONS_FROM (2026.10.16), and the hub's
  firmware-change resync now also fires across that version, so a frame gets
  its lists, on the scene it shows, the moment it starts enforcing them. The
  scenes.json route and zip download stamp always.
- Lint: config.json permissions must be the four names; a hand-set list short
  of the code is a warning. Legacy Nim code shells out whatever the node
  calls itself; JavaScript code never does.
- Store: category headers text-lg -> text-2xl, scene names text-sm ->
  text-base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariusandra mariusandra changed the title Scene asset access: a per-scene setting, flagged wherever the scene shows up Scene permissions: apps declare, scenes add up, the frame enforces Oct 17, 2026
FrameOS Bot and others added 2 commits October 17, 2026 01:43
…rom its code

A JS app's config.json `permissions` now always follows its code. A list
written by hand is replaced on the next save; if a manual grant is ever
needed it can be a permission of its own in the list.

The AI linter's shortfall warning covers every app with a list (it only
covered hand-set lists before), so a stale list in a scene saved outside
the editor still reaches the model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariusandra
mariusandra merged commit 812bbb7 into main Oct 17, 2026
25 checks passed
@mariusandra
mariusandra deleted the store-scene-asset-sandbox branch October 17, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant