Commit graph

93 commits

Author SHA1 Message Date
7e92b72a93 feat(pack): install accepts .app.tar.zst archives directly
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.
2026-03-11 15:49:34 +01:00
d2cb693c55 feat(appd): MountOrchestrator -- EROFS+dm-verity image mount on app launch
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.
2026-03-11 15:47:23 +01:00
97ea969075 feat: weft-mount-helper -- setuid helper for EROFS+dm-verity mounts
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.
2026-03-11 15:43:59 +01:00
add4d92945 feat(pack): build-image and build-verity subcommands
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.
2026-03-11 15:39:18 +01:00
12fa53a585 feat(pack): bundle and unbundle subcommands for dist packaging
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.
2026-03-11 15:37:53 +01:00
98a21da734 feat(runtime): seccomp blocklist filter via optional seccomp feature
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.
2026-03-11 15:34:21 +01:00
ec4cc272af feat(pack): Ed25519 package signing -- generate-key, sign, verify subcommands 2026-03-11 15:29:49 +01:00
71b7bdf657 feat(appd): wrap runtime in systemd-run cgroup scope when user session is active
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.
2026-03-11 15:25:04 +01:00
c5a47a05b4 feat(appd,pack): capability dispatch -- map wapp.toml capabilities to --preopen args
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.
2026-03-11 15:20:51 +01:00
84eb39db96 feat(runtime): add weft:app/notify WIT package and notify-ready host interface
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
2026-03-11 15:15:11 +01:00
b2ac279dc5 feat(runtime): add --preopen and --ipc-socket CLI arguments
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.
2026-03-11 15:10:11 +01:00
e56daf6570 feat(runtime): upgrade to WASI Preview 2 + Component Model
Replaces the wasmtime-runtime run_module implementation:
- wasmtime::Module → wasmtime::component::Component
- wasmtime::Linker<WasiCtx> → wasmtime::component::Linker<State>
- wasmtime_wasi::add_to_linker → wasmtime_wasi::add_to_linker_sync
- _start typed func call → wasmtime_wasi::bindings::sync::Command::instantiate + call_run

Config now sets wasm_component_model(true). State struct implements WasiView
(ctx + table). app.wasm must be a WASI 0.2 component; core modules are no
longer supported.
2026-03-11 15:03:16 +01:00
d425fa8328 feat(servo-shell): implement weft-shell-protocol Wayland client
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.
2026-03-11 14:59:58 +01:00
2a9f034815 feat(servo-shell): add servo-embed feature gate and embedder contract
Adds src/embedder.rs with the full Servo embedding implementation behind
#[cfg(feature = " servo-embed)]:
2026-03-11 14:52:13 +01:00
6b428e5a47 feat(appd): add compositor IPC client; send AppSurfaceCreated/Destroyed on session lifecycle
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.
2026-03-11 14:40:55 +01:00
69d29ee3a8 feat(protocol): add wl_surface arg to create_window in weft-shell-unstable-v1
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.
2026-03-11 14:33:17 +01:00
ca2cc38d4d feat(compositor): add appd IPC server (Unix socket, length-prefixed MessagePack framing)
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.
2026-03-11 14:29:22 +01:00
a75c8946fc feat(ipc-types): add weft-ipc-types crate with compositor-appd message types and frame framing 2026-03-11 14:17:48 +01:00
5d7c0bdf79 feat(appd): add version field to AppInfo; surface it in launcher tile tooltip
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.
2026-03-11 13:15:09 +01:00
7a2014027a test(pack): add missing-wasm and missing-ui-entry check_package tests 2026-03-11 13:07:45 +01:00
afffe29090 docs: update README to accurately reflect current implementation state 2026-03-11 12:59:24 +01:00
c88c948575 fix(appd): exclude Stopped sessions from running_sessions; add regression test
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.
2026-03-11 12:53:07 +01:00
de8939a72e test(appd): add ws_port default and override tests 2026-03-11 12:46:15 +01:00
bded9455f5 test(appd): add appd_socket_path tests; run appd tests single-threaded
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.
2026-03-11 12:40:05 +01:00
71597580ba fix(appd): abort TerminateApp during startup phase promptly
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
2026-03-11 12:30:21 +01:00
488900a5db test(appd): add supervisor spawn-failure test; verifies Stopped broadcast when binary is missing 2026-03-11 12:24:03 +01:00
e80502b184 test(runtime): add resolve_package tests for found and not-found cases 2026-03-11 12:19:17 +01:00
dbcc9965e9 test(appd): add roundtrip tests for TerminateApp, Error, and AppState IPC variants 2026-03-11 12:12:20 +01:00
60256138a9 fix(shell): log IPC ERROR responses to console 2026-03-11 12:06:56 +01:00
d2fa616b00 feat(shell): refresh installed apps list on every launcher open 2026-03-11 12:02:11 +01:00
65a10723b3 fix(shell): clear stale taskbar entries on WebSocket reconnect
When weft-appd restarts, session IDs reset to 0. Without clearing,
RUNNING_APPS repopulation after reconnect would coexist with stale
entries from the previous session. Removing all weft-taskbar-app
elements on the open event ensures the taskbar reflects only the
current appd session registry.
2026-03-11 12:01:33 +01:00
5e7675c043 test(pack): add list_installed_roots tests; run pack tests single-threaded
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.
2026-03-11 12:00:44 +01:00
cab3a4a956 feat(pack): add list subcommand to show installed packages
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.
2026-03-11 11:57:08 +01:00
826f144d9d test(appd): add roundtrip tests for AppInfo and InstalledApps IPC variants 2026-03-11 11:51:04 +01:00
abdefa3388 test(appd): add QueryAppState dispatch test for unknown session returning NotFound 2026-03-11 11:48:22 +01:00
7a07e46c55 fix(appd): broadcast AppState::Stopped on runtime spawn failure
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.
2026-03-11 11:45:17 +01:00
71c6741fae feat(shell): add terminate button to taskbar app entries
ensureTaskbarEntry() now creates a label span and an inline x close
button. The x button is hidden until the taskbar entry is hovered, then
sends TERMINATE_APP via WebSocket on click. CSS for weft-taskbar-app
and .task-close added (flex layout, hover reveal, red tint on x hover).
2026-03-11 11:40:25 +01:00
a3a826058e feat(shell): close launcher on Escape key and click-outside 2026-03-11 11:39:21 +01:00
e5ec05ce2c test(appd): assert AppState::Stopped broadcast in supervisor integration test
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.
2026-03-11 11:38:33 +01:00
e83be20798 fix(appd): make appd.wsport write non-fatal when XDG_RUNTIME_DIR is unset
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.
2026-03-11 11:36:47 +01:00
eef9ecc24a test(appd): add QueryInstalledApps dispatch test; fix weft-runtime test race
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.
2026-03-11 11:32:26 +01:00
0bcb6b1bf6 fix(appd): signal all supervisors to abort on clean shutdown
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.
2026-03-11 11:28:29 +01:00
e1c15ea463 feat(appd): add QueryInstalledApps IPC request; wire launcher in system UI 2026-03-11 11:23:46 +01:00
d6de84b4c7 fix(appd): broadcast AppState::Stopped on READY timeout
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.
2026-03-11 11:19:09 +01:00
3315b158db feat(appd): broadcast AppState::Stopped when supervised process exits
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.
2026-03-11 11:16:28 +01:00
68e1f82ca7 fix(appd): drain module stdout after READY signal to prevent pipe stall
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.
2026-03-11 11:14:18 +01:00
a409b954ab fix(appd): handle SIGTERM for clean shutdown under systemd
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.
2026-03-11 11:06:01 +01:00
01a4969883 fix(appd): write actual bound WebSocket port to appd.wsport file
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.
2026-03-11 11:00:13 +01:00
5bd4857a67 fix(servo-shell): show app name in APP_READY notification
system-ui.html: APP_READY handler extracts the last component of
msg.app_id (e.g. 'notes' from 'com.example.notes') and shows
'<name> is ready'. Falls back to the session_id if app_id is absent.
2026-03-11 10:51:16 +01:00
dbe44bd0e0 feat(appd): include app_id in AppReady broadcast
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.
2026-03-11 10:50:41 +01:00