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:
- Declare the permission in their
plugin.json. - Request it at runtime via the runtime's permission API
(
AnvilPermissions.request("android.permission.CAMERA")) or, on iOS, the correspondingAVCaptureDevice/CLLocationManagerflow. - Surface
<plugin>/permission-deniedif 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:
| API | Scope |
|---|---|
document.cookie | slm-eximia://app |
localStorage | slm-eximia://app |
indexedDB | slm-eximia://app |
cacheStorage | slm-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.