install_package_to detects .app.tar.zst input by extension, unbundles
to a temp directory, derives the app_id from the archive filename stem,
then proceeds with the standard check + copy install path.
Directory-based installs are unchanged.
Test: install_package_from_archive.
Add crates/weft-appd/src/mount.rs with MountOrchestrator.
On each app launch, supervise() calls MountOrchestrator::mount_if_needed
which looks for <app_id>.app.img, <app_id>.hash, and <app_id>.roothash
in the app store roots. If all three exist and weft-mount-helper is
available (WEFT_MOUNT_HELPER env or /usr/lib/weft/ default paths), it:
- creates /tmp/weft-mnt-<session_id>/<app_id>/
- invokes weft-mount-helper mount to set up dm-verity and EROFS mount
- sets WEFT_APP_STORE=/tmp/weft-mnt-<session_id> in the child env so
the runtime resolves the package from the mounted read-only image
After the process exits, umount() invokes weft-mount-helper umount and
removes the temporary directory.
Falls back to directory-based install silently if no image is found,
mount-helper is absent, or the mount fails.
Test: find_image_returns_none_when_absent.
New crate: weft-mount-helper. A privileged helper binary that sets up
dm-verity devices and mounts EROFS images for app isolation.
Commands:
mount <img> <hash_dev> <root_hash> <mountpoint>
- opens a named dm-verity device via veritysetup open
- mounts the resulting /dev/mapper/<name> as EROFS read-only
- cleans up the dm device if mount fails
umount <mountpoint>
- unmounts the mountpoint
- closes the dm-verity device via veritysetup close
Device naming: derives a stable name from the mountpoint path, limited
to 31 chars (DM limit), always prefixed weft-. Root check reads
/proc/self/status euid rather than using unsafe libc calls.
Tests: device_name_sanitizes_path, device_name_truncates_long_paths.
build-image <dir> [--out <img>]: creates an EROFS image by invoking
mkfs.erofs. Output filename defaults to <app_id>.app.img. Fails if
the output file already exists or mkfs.erofs is unavailable.
build-verity <img> [--out <hash>]: creates a dm-verity hash tree by
invoking veritysetup format. Writes the hash device to <stem>.hash
(or --out path). Extracts the root hash from veritysetup output and
writes it to <img>.roothash. Fails if veritysetup is unavailable.
Both subcommands shell out to external tools (erofs-utils and
cryptsetup-bin respectively) and return an error with an installation
hint if the binary is not found.
Add bundle <dir> [--out <dir>] and unbundle <archive> [--out <dir>]
subcommands to weft-pack.
bundle: validates the package, reads app_id from wapp.toml, writes
<app_id>.app.tar.zst to the output directory (default: current dir).
Archive root is <app_id>/ so extraction reproduces the package directory.
Fails if the archive already exists.
unbundle: decompresses and extracts a .app.tar.zst into the output
directory (default: current dir).
Compression level 0 (zstd default). No symlinks followed.
Dependencies added: tar 0.4, zstd 0.13.
Test: bundle_and_unbundle_roundtrip.
Add seccomp feature flag (seccompiler + libc, Linux-only, optional).
When compiled with --features seccomp, weft-runtime installs a
SECCOMP_MODE_FILTER immediately after argument parsing, before any
package resolution or WASM execution.
Filter strategy: default-allow with explicit KillProcess rules for
high-risk syscalls a WASM runtime process has no legitimate need for:
ptrace, process_vm_readv/writev, kexec_load, personality, syslog,
reboot, mount/umount2, setuid/setgid/setreuid/setregid/setresuid/
setresgid, chroot, pivot_root, init_module/finit_module/delete_module,
bpf, perf_event_open, acct.
The feature is off by default so the standard build and tests are
unaffected. Enable in production service builds with --features seccomp.
supervise() checks /systemd/private to detect an active
user systemd session. When present (and WEFT_DISABLE_CGROUP is unset),
the runtime binary is launched via:
systemd-run --user --scope --wait --collect --slice=weft-apps.slice -p CPUQuota=200% -p MemoryMax=512M -- <bin> ...
This places each app in a transient weft-apps.slice scope with default
resource limits from the blueprint. The --wait flag keeps systemd-run
alive so child.wait()/child.kill() remain correct.
When no user session is present the command is built directly as before.
WEFT_DISABLE_CGROUP=1 bypasses the wrapping unconditionally.
Add capabilities field to weft-pack PackageMeta (optional Vec<String>).
Print cap: lines in weft-pack info output when capabilities are declared.
In weft-appd:
- Make app_store_roots pub(crate) so runtime.rs can use it.
- Add resolve_preopens(app_id) in runtime.rs: reads wapp.toml from the
package store, extracts capabilities, maps each to a (host, guest) pair:
fs:rw:app-data / fs:read:app-data -> ~/.local/share/weft/apps/<id>/data :: /data
fs:rw:xdg-documents / fs:read:xdg-documents -> ~/Documents :: /xdg/documents
Unknown capabilities are logged at debug level and skipped.
- supervise() calls resolve_preopens() and appends --preopen HOST::GUEST
flags before spawning the runtime binary.
Adds crates/weft-runtime/wit/weft-app.wit defining package weft:app@0.1.0
with interface notify { ready: func() }.
In the wasmtime-runtime path:
- Registers weft:app/notify@0.1.0 in the component linker before instantiation
- ready() prints
weft-runtime now parses optional flags after <app_id> <session_id>:
--preopen HOST::GUEST pre-opens a host directory at GUEST path in the
WASI filesystem (HOST::GUEST or HOST for same path)
--ipc-socket PATH sets WEFT_IPC_SOCKET env var inside the component
wasmtime-runtime path applies preopened dirs via cap_std and WasiCtxBuilder,
and injects WEFT_IPC_SOCKET when --ipc-socket is present. Stub path ignores
both flags.
weft-appd: SessionRegistry gains ipc_socket field (set to the appd Unix
socket path in run()), extracted alongside compositor_tx in dispatch(), and
forwarded to supervise() as ipc_socket_path. supervise() passes
--ipc-socket <path> to the spawned runtime when present.
cap-std added as optional dep under wasmtime-runtime feature.
Adds src/shell_client.rs: connects to the WEFT compositor via
WAYLAND_DISPLAY, binds zweft_shell_manager_v1, and calls create_window
for the system UI shell slot (app_id org.weft.system.shell, role panel,
wl_surface null until Servo surface is wired in a later task).
Implements Dispatch for WlRegistry, ZweftShellManagerV1, and
ZweftShellWindowV1. Handles all four window events: configure,
focus_changed, window_closed (calls destroy), and presentation_feedback.
run() in main.rs calls ShellClient::connect() best-effort before
embed_servo; logs a warning if the compositor is not running rather than
propagating the error.
Adds crates/weft-appd/src/compositor_client.rs: async Tokio client that connects to the
compositor's Unix socket (/weft/compositor.sock or WEFT_COMPOSITOR_SOCKET),
retrying every 2s on failure and 500ms on write error. Incoming CompositorToAppd frames are
decoded and logged (SurfaceReady, ClientDisconnected).
Wires compositor_tx into SessionRegistry. The supervise task now sends AppSurfaceCreated
(with child PID) immediately after process spawn, and AppSurfaceDestroyed when the process
exits. All three existing supervisor tests updated to pass None for compositor_tx.
Adds an optional (allow-null) wl_surface argument to zweft_shell_manager_v1::create_window.
Shell-owned windows pass null; app-backed windows pass the backing wl_surface so the
compositor can correlate the surface with a session_id from weft-appd. Updates
WeftShellWindowData to store the surface, and updates the CreateWindow handler in state.rs
and the two protocol unit tests.
Adds weft-compositor/src/appd_ipc.rs: WeftAppdIpc state, setup() registers a calloop
UnixListener source. Accepted connections are registered as edge-triggered read sources.
Incoming AppdToCompositor frames are decoded and dispatched; AppSurfaceCreated records
pid->session mapping in pending_pids for later wl_surface association. Wires into both
the DRM and Winit backends. Socket path: /weft/compositor.sock or
WEFT_COMPOSITOR_SOCKET override.
WappPackage and AppInfo both gain a version field. scan_installed_apps()
reads it from wapp.toml and includes it in InstalledApps responses.
system-ui.html shows it in the title tooltip as 'com.example.app v1.0.0'.
All roundtrip and integration tests updated.
running_sessions() was returning all sessions regardless of state.
Stopped sessions would reappear in the taskbar on reconnect since
QUERY_RUNNING is sent on every WebSocket open. The filter now matches
the UI expectation: only Starting and Running sessions are returned.
Two new tests cover appd_socket_path():
- appd_socket_path_uses_override_env: WEFT_APPD_SOCKET takes precedence
- appd_socket_path_errors_without_xdg_and_no_override: returns error when
both WEFT_APPD_SOCKET and XDG_RUNTIME_DIR are unset
wsl-test.sh: add --test-threads=1 for weft-appd to prevent WEFT_RUNTIME_BIN
races between the supervisor integration tests.
Before this fix, TerminateApp sent while a process was waiting for its
READY signal was not acted on until the 30-second timeout fired.
abort_rx is now included in the tokio::select! that wraps wait_for_ready,
so the child is killed and AppState::Stopped broadcast as soon as the
abort is received, regardless of where in the startup sequence it fires.
test: supervisor_abort_during_startup_broadcasts_stopped
Two new tests mirror the weft-runtime package_store_roots tests:
- list_installed_roots_uses_weft_app_store_when_set
- list_installed_roots_includes_system_path
wsl-test.sh: add --test-threads=1 for weft-pack to prevent
WEFT_APP_STORE env var races between install, uninstall, and the
new list_roots tests.
list_installed_roots() searches WEFT_APP_STORE, then
~/.local/share/weft/apps, then /usr/share/weft/apps (same
priority order as weft-runtime and weft-appd). list_installed()
deduplicates by app_id (first root wins), sorts alphabetically within
each root, and prints id/name/version per line. Prints 'no packages
installed' when the store is empty or absent.
If WEFT_RUNTIME_BIN is set but the binary cannot be spawned (missing,
not executable, etc.), supervise() now transitions the session to
Stopped and broadcasts AppState::Stopped instead of returning an error
that left the session permanently stuck in Starting.
supervisor_transitions_through_ready_to_stopped now checks both
broadcast messages: AppReady (on READY signal) and AppState::Stopped
(on process exit), covering the path added in 3315b15.
write_ws_port failure is now logged as a warning rather than propagating
an error that would crash the service. Error context strings are added
to create_dir_all and write failures so the warning is actionable.
main.rs: add dispatch_query_installed_returns_installed_apps to verify
the QueryInstalledApps arm returns Response::InstalledApps.
wsl-test.sh: run weft-runtime tests with --test-threads=1 to prevent
the WEFT_APP_STORE env var race between package_store_roots_includes_
system_path and package_store_roots_uses_weft_app_store_when_set.
SessionRegistry::shutdown_all() clears abort_senders, dropping all
oneshot senders. Each supervised process's abort_rx fires, causing
supervise() to kill the child. A 200ms yield after shutdown_all gives
the tokio runtime time to schedule the abort handling before the
process exits and the socket file is removed.
runtime.rs: the READY-timeout early-return path now broadcasts
AppState::Stopped before returning so WebSocket clients see the
session disappear when a module fails to signal readiness within 30s.
runtime.rs: after the child exits (natural exit or abort), supervise()
now broadcasts AppState { session_id, state: Stopped } in the same
lock scope as set_state. WebSocket clients receive the notification
without needing to poll QueryAppState or call TerminateApp.
wait_for_ready() now returns the BufReader<ChildStdout> with the READY
line already consumed. supervise() spawns drain_stdout() on that reader
so any subsequent module output is forwarded to the trace log and the
pipe buffer never fills up.
Without this, a long-running Wasm module that writes to stdout after
printing READY would eventually block waiting on a full pipe.
run() now registers a SIGTERM handler (unix-only, cfg-gated) alongside
the existing SIGINT handler. Both break the accept loop and allow the
Unix socket to be removed before exit.
On non-Unix targets the SIGTERM arm uses std::future::pending so the
select! shape is unchanged at the type level.
ws_listener.local_addr().port() is used instead of the configured
ws_port value. This is correct when WEFT_APPD_WS_PORT=0 lets the OS
assign an ephemeral port; the file reflects the real listening port.
ipc.rs: AppReady { session_id, app_id: String }.
runtime.rs: supervise() passes app_id (already in scope as parameter)
when building the AppReady broadcast message.
main.rs: supervisor integration test updated to use .. to ignore
app_id in the AppReady pattern match.
ipc.rs: LaunchAck gains app_id: String field so callers receive the
app identifier alongside the session handle in a single response.
main.rs: dispatch::LaunchApp constructs LaunchAck { session_id, app_id }
using the app_id that was already in scope.
Tests updated: dispatch_launch_returns_ack now asserts app_id value;
dispatch_terminate_known_returns_stopped and
dispatch_query_app_state_returns_starting use .. to ignore app_id.
ipc.rs:
- Add SessionInfo { session_id: u64, app_id: String } struct.
- Change RunningApps { session_ids: Vec<u64> } to
RunningApps { sessions: Vec<SessionInfo> } so callers can display
meaningful app names without a follow-up QueryAppState round-trip.
- Add session_info_roundtrip test.
main.rs:
- Add SessionEntry { app_id: String, state: AppStateKind } to store
app_id alongside state in SessionRegistry.
- launch() stores app_id in the entry.
- running_sessions() replaces running_ids(); returns Vec<SessionInfo>.
- state() reads from SessionEntry.state.
- set_state() writes to SessionEntry.state.
- QueryRunning dispatch uses running_sessions().
- Test registry_running_ids_reflects_live_sessions renamed to
registry_running_sessions_reflects_live_sessions and updated to
assert both session_id and app_id fields.
- dispatch_query_running test asserts app_id values are present.
system-ui.html:
- RUNNING_APPS handler uses msg.sessions[].{session_id,app_id}.
- ensureTaskbarEntry(sessionId, appId): shows the last component of the
reverse-domain app ID as the taskbar label; sets data-app-id attribute;
tooltip shows full app ID and session number.
- LAUNCH_ACK handler passes null for appId (session ID only available
at launch time; app_id arrives in RUNNING_APPS on reconnect).
appd_ws_port() -> u16:
- Checks WEFT_APPD_WS_PORT env var first.
- Falls back to reading XDG_RUNTIME_DIR/weft/appd.wsport.
- Falls back to hardcoded default 7410.
run() now calls appd_ws_port() and passes the result to embed_servo.
embed_servo signature updated to accept ws_port: u16.
When the Servo embedder is implemented, it injects the port as
window.WEFT_APPD_WS_PORT before loading the system UI HTML.
Cargo.toml:
- New feature: wasmtime-runtime = [dep:wasmtime, dep:wasmtime-wasi]
- Default is off so the normal build remains lightweight.
- wasmtime 30 and wasmtime-wasi 30 added as optional dependencies.
src/main.rs:
- run_module(wasm_path) replaces the inline stub.
- cfg(not(feature = wasmtime-runtime)): prints READY and returns.
Preserves all existing test and development behaviour unchanged.
- cfg(feature = wasmtime-runtime): creates a Wasmtime Engine + Module,
builds a WASI linker with inherited stdout/stderr, prints READY, then
instantiates the module and calls _start.
READY is printed before _start so weft-appd can record the session as
Running before the app enters its event loop.
The production service binary is built with:
cargo build -p weft-runtime --release --features wasmtime-runtime
check_package now reads the first 4 bytes of runtime.module and rejects
files that do not begin with the Wasm magic number (0x00 0x61 0x73 0x6D).
An unreadable or too-short file is treated as invalid.
is_wasm_module(path): opens the file, reads 4 bytes, compares to MAGIC.
Test added: check_package_bad_wasm_magic - writes NOT_WASM to app.wasm,
asserts check fails with a message containing bad magic bytes.
weft-pack:
- uninstall <app_id>: validates app ID, resolves store root, removes
the installed package directory. Fails with an error if the package
is not present or the app ID is malformed.
- Extracted install_package_to(dir, root) and
uninstall_package_from(app_id, root) inner functions so tests can
drive them directly without touching process env vars (avoids parallel
test env-var races).
- install_package / uninstall_package remain the CLI-facing wrappers
that call resolve_install_root().
Tests added (2):
- install_package_copies_to_store: writes a valid temp package, calls
install_package_to, verifies all files are present, confirms a second
install fails.
- uninstall_package_removes_directory: installs then uninstalls,
verifies directory is removed, confirms a second uninstall fails.
Both tests use process-ID-derived paths to avoid cross-test collisions.
weft-pack:
- install <dir>: validates the package (runs check), resolves the user
app store root (WEFT_APP_STORE > ~/.local/share/weft/apps), copies
the package directory to <root>/<app_id>/. Fails if the destination
already exists.
- resolve_install_root(): replaces the unused _resolve_store_roots;
returns a single writable root rather than a search list.
- copy_dir(): recursive directory copy using std::fs only; no new deps.
- Updated usage text to include all three subcommands.
weft-servo-shell: removed stale implementation-note comment from
embed_servo stub.