Eximia Download

Eximia docs

Security model

What an SLMEximia-shipped app can and can't do, what defaults the runtime sets, and where the trust boundaries lie.


Trust model

┌──────────────────────────────────────────────────────────┐
│  TRUSTED:                                                │
│  · SLM Eximia runtime (built into the app)                    │
│  · Plugins bundled into the app (selected at build time) │
│  · App native code (host activity / view controller)     │
│                                                          │
│  UNTRUSTED:                                              │
│  · JS code in the WebView (treated as input)             │
│  · Network responses (validated by plugins or the app)   │
│  · User input                                            │
└──────────────────────────────────────────────────────────┘

The WebView's JS is treated as untrusted by default. Even though the JS is shipped inside the app bundle, an XSS bug or compromised CDN could make it run arbitrary code. The bridge is the gatekeeper between this untrusted JS and the native privileges of plugins.


Bridge gating

By default, every plugin call from JS is allowed. The bridge does not implement per-action permissions in v1.0.

This is a deliberate choice for v1.0:

  • The plugin code itself enforces what it does (e.g. CameraPlugin prompts for OS permission before opening the camera).
  • Adding bridge-level gating without a clear threat model produces cargo-cult security.

Future versions may add per-action capabilities (e.g. "this app can call Camera.take but not Camera.exportRaw"). Designed in v1.1, not v1.0.


WebView hardening (defaults)

On Android, SLMEximiaWebView sets:

settings.javaScriptEnabled = true                     // required for bridge
settings.allowFileAccess = false                      // no file://
settings.allowFileAccessFromFileURLs = false
settings.allowUniversalAccessFromFileURLs = false
settings.allowContentAccess = false                   // no content://
settings.domStorageEnabled = true                     // localStorage
settings.databaseEnabled = false                      // deprecated WebSQL
settings.mediaPlaybackRequiresUserGesture = false     // app audio is OK
settings.setSupportZoom(false)
settings.javaScriptCanOpenWindowsAutomatically = false

WebViewClient.shouldOverrideUrlLoading returns true (handled by the runtime) for any URL whose scheme is not slm-eximia://, https://, or http:// — preventing javascript: and intent:// from being triggered by malicious JS.

On iOS, equivalent settings on the WKWebViewConfiguration:

cfg.preferences.javaScriptEnabled = true
cfg.preferences.javaScriptCanOpenWindowsAutomatically = false
cfg.allowsAirPlayForMediaPlayback = false
cfg.mediaTypesRequiringUserActionForPlayback = []
cfg.upgradeKnownHostsToHTTPS = true                   // iOS 14+

Content Security Policy

The runtime injects a default CSP into every HTML response served via the scheme handler. Apps can override by setting their own <meta http-equiv> or by configuring the runtime.

Default:

default-src 'self' slm-eximia:;
img-src 'self' slm-eximia: https: data: blob:;
media-src 'self' slm-eximia: https: blob:;
font-src 'self' slm-eximia: data: https:;
connect-src 'self' slm-eximia: https: wss:;
script-src 'self' slm-eximia:;
style-src 'self' slm-eximia: 'unsafe-inline';

The 'unsafe-inline' for style-src is unfortunate but matches what most apps need; tightening it is documented in tightening-csp.md (future work).


Path traversal

The scheme handlers (Android and iOS) MUST reject any URL whose normalised path escapes the bundled www/ root. Implementation:

// Pseudocode
val canonical = File(wwwRoot, requestedPath).canonicalFile
if (!canonical.startsWith(wwwRoot.canonicalFile)) return notFound()

The corresponding iOS check uses URL.standardizedFileURL and compares the path prefix.

This is verified by the test scheme_traversal_blocked in both runtimes.


Native permissions

Plugins that need OS permissions (camera, microphone, location, etc.) MUST:

  1. Declare the permission in their plugin.json.
  2. Request it at runtime via the runtime's permission API (AnvilPermissions.request("android.permission.CAMERA")) or, on iOS, the corresponding AVCaptureDevice / CLLocationManager flow.
  3. Surface <plugin>/permission-denied if the user refuses.

Plugins MUST NOT pre-emptively request permissions at app startup. The runtime warns in debug builds when this happens.


Vault: keystores and provisioning profiles

The forge-api build pipeline signs APKs with keystores and IPAs with provisioning profiles. These live in /WORK/forge/vault/ on the build server and are never copied into the runtime, plugin code, or app artefact source trees. SLMEximia itself does not touch them.

See ../cli/docs/commands/build.md for how the CLI requests signed builds from forge-api.


Plugin trust boundary

Plugins are part of the trusted code: they have unrestricted access to native APIs. The decision to bundle a plugin is the app team's choice at build time. SLMEximia does not sandbox plugins.

If a future plugin marketplace allows third-party plugins to be installed at runtime (post-v1.0), a stricter model is needed: signed plugin packages, capability declarations, runtime-enforced permission prompts. Out of scope for v1.0.


Cookies and storage

Per-origin storage (slm-eximia://app) is isolated by the WebView itself:

APIScope
document.cookieslm-eximia://app
localStorageslm-eximia://app
indexedDBslm-eximia://app
cacheStorageslm-eximia://app

Plugins that want their own persistent storage (e.g. encrypted secrets) SHOULD use the host OS keystore (Android Keystore, iOS Keychain) and expose access via plugin actions — not by writing to the WebView's storage areas.


Logging

The runtime never logs args or value payloads in release builds. Plugin authors must not log sensitive data themselves; the runtime cannot enforce this, but SLMEximiaLog.tag("plugin").debug(...) is a no-op in release.