Commit graph

123 commits

Author SHA1 Message Date
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
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
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
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
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
b5bf2e538a feat(appd): include app_id in LaunchAck response
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.
2026-03-11 10:46:28 +01:00
fdeb440766 feat(appd): include app_id in RunningApps response; update system UI
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).
2026-03-11 10:42:40 +01:00
d6ede23183 feat(servo-shell): wire appd WebSocket port discovery at startup
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.
2026-03-11 10:31:33 +01:00
6d88104f28 feat(runtime): add wasmtime-runtime feature gate for real Wasm execution
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
2026-03-11 10:26:41 +01:00
5cff1f4412 feat(pack): validate Wasm module magic bytes in check
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.
2026-03-11 10:21:43 +01:00
b2bb76125f feat(pack): add uninstall subcommand and full install/uninstall tests
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.
2026-03-11 09:54:39 +01:00
265868bf67 feat(pack): add install subcommand; clean up servo-shell stub comment
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.
2026-03-11 09:45:31 +01:00
ffae164747 feat(pack): add weft-pack package validator tool
New crate: weft-pack — command-line tool for validating WEFT application
package directories against the app-package-format spec.

src/main.rs:
- check <dir>: loads wapp.toml, validates app ID format, verifies
  package.name is non-empty and <=64 chars, confirms runtime.module and
  ui.entry files exist. Prints 'OK' on success or the list of errors.
- info <dir>: prints all manifest fields to stdout.
- load_manifest(): reads and parses wapp.toml with toml crate.
- is_valid_app_id(): enforces reverse-domain convention (>=3 components,
  each starting with a lowercase letter, digits allowed, no hyphens or
  uppercase).

Tests (5):
- app_id_valid: accepts well-formed reverse-domain IDs.
- app_id_invalid: rejects two-component, uppercase, hyphen, empty IDs.
- check_package_missing_manifest: error when wapp.toml is absent.
- check_package_valid: full happy-path with real temp files.
- check_package_invalid_app_id: error on a hyphenated app ID.

New deps: toml 0.8, serde 1 (derive).
Added weft-pack to workspace Cargo.toml; wsl-test.sh extended.
2026-03-11 09:40:34 +01:00
1e4ced9a39 feat(appd): implement TerminateApp process signaling via abort channel
SessionRegistry now tracks a oneshot abort sender per active session:
- abort_senders: HashMap<u64, oneshot::Sender<()>> field added.
- register_abort(session_id): creates the channel, stores the sender,
  returns the receiver to the supervise task.
- terminate(): removes the session state AND drops the abort sender,
  closing the channel and triggering the receiver in supervise.

runtime::supervise() now accepts abort_rx: oneshot::Receiver<()>:
- After the READY signal is received, the process-wait loop uses
  tokio::select! on child.wait() vs abort_rx.
- On abort: logs intent, calls child.kill(), then sets state Stopped.
- On natural exit: logs exit status, sets state Stopped.

dispatch::LaunchApp: calls register_abort immediately after launch,
passes the receiver to the spawned supervise task.

Integration test updated to pass the abort receiver.
2026-03-11 09:37:09 +01:00
f38f2eef76 feat(runtime): add weft-runtime crate skeleton
New crate: weft-runtime — the child process spawned by weft-appd to
execute WEFT application packages.

src/main.rs:
- Parses CLI arguments: <app_id> <session_id> (as per the supervisor
  contract in runtime.rs).
- resolve_package(): searches user store
  (~/.local/share/weft/apps/<app_id>) then system store
  (/usr/share/weft/apps/<app_id>) for a wapp.toml manifest. Overridden
  by WEFT_APP_STORE env var.
- Verifies app.wasm exists in the resolved package directory.
- Stubs Wasmtime execution with a TODO comment; prints 'READY' to
  stdout and exits cleanly so weft-appd's supervisor can complete the
  session lifecycle during development and integration testing.

Tests (2):
- package_store_roots_includes_system_path: system store path present.
- package_store_roots_uses_weft_app_store_when_set: WEFT_APP_STORE
  override replaces default search list.

Also:
- Added weft-runtime to workspace Cargo.toml members.
- wsl-test.sh: added cargo test -p weft-runtime.
2026-03-11 09:27:30 +01:00
f47150cec8 test(appd): add runtime supervisor integration test
supervisor_transitions_through_ready_to_stopped (unix only):
- Writes a temp shell script that prints 'READY' and exits.
- Sets WEFT_RUNTIME_BIN to the script path; restores env after test.
- Calls runtime::supervise() and verifies final session state is Stopped.
- Verifies AppReady was broadcast via the registry broadcast channel.
- Runs with tokio flavor='current_thread' to avoid concurrent env
  mutation. Wraps set_var/remove_var in unsafe blocks (required since
  Rust 1.93).
2026-03-11 09:24:34 +01:00
86d0011016 feat(appd): implement runtime supervisor with process spawning and READY signal
runtime.rs — process lifecycle manager:
- supervise(session_id, app_id, registry): spawns the weft-runtime child
  process identified by WEFT_RUNTIME_BIN env var. If unset, logs debug
  and returns immediately (no-op until runtime binary is available).
- Child process invoked as: <WEFT_RUNTIME_BIN> <app_id> <session_id>
  with stdout/stderr piped, stdin closed.
- wait_for_ready(): reads stdout line-by-line; returns Ok(()) on first
  line matching 'READY'; returns Err if stdout closes without it.
- 30-second READY_TIMEOUT via tokio::time::timeout; on expiry, kills
  the child and transitions session to Stopped.
- On success: sets session state to Running, broadcasts AppReady to all
  connected WebSocket clients via registry broadcast channel.
- drain_stderr(): async task that forwards child stderr lines to tracing
  at WARN level for observability.
- On process exit: sets session state to Stopped regardless of exit code.

main.rs — wiring:
- SessionRegistry now owns broadcast::Sender<Response>; Default creates
  the channel internally. Added set_state(), subscribe(), broadcast()
  methods. Removed standalone broadcast_tx from run(); WS handlers
  subscribe via registry.lock().await.subscribe().
- dispatch::LaunchApp spawns a tokio task calling runtime::supervise
  immediately after creating the session. supervise is a no-op when
  WEFT_RUNTIME_BIN is unset, so existing tests are unaffected.

Cargo.toml: added tokio 'process' and 'time' features.
2026-03-11 09:17:20 +01:00
7cebac4188 feat(appd): add WebSocket UI endpoint for Servo shell integration
Implements the weft-appd WebSocket server that allows the system-ui.html
page running inside Servo to send requests and receive push notifications
without requiring custom SpiderMonkey bindings.

ws.rs — WebSocket connection handler:
- Accepts a tokio TcpStream, performs WebSocket handshake via
  tokio-tungstenite accept_async.
- Reads JSON Text frames, deserializes as Request (serde_json), calls
  dispatch(), sends Response as JSON Text.
- Subscribes to a broadcast::Receiver<Response> for server-push
  notifications (APP_READY, etc.); forwards to client via select!.
- Handles close frames, partial errors, and lagged broadcast gracefully.

main.rs — server changes:
- broadcast::channel(16) created at startup; WebSocket handlers
  subscribe for push delivery.
- TcpListener bound on 127.0.0.1:7410 (default) or WEFT_APPD_WS_PORT.
- ws_port() / write_ws_port(): port written to
  XDG_RUNTIME_DIR/weft/appd.wsport for runtime discovery.
- WS accept branch added to the main select! loop alongside Unix socket.

ipc.rs — Response and AppStateKind now derive Clone (required by
broadcast::Sender<Response>).

system-ui.html — appd WebSocket client:
- appdConnect(): opens ws://127.0.0.1:<port>/appd with exponential
  backoff reconnect (1s → 16s max).
- On open: sends QUERY_RUNNING to populate taskbar with live sessions.
- handleAppdMessage(): maps LAUNCH_ACK and RUNNING_APPS to taskbar
  entries; APP_READY shows a timed notification; APP_STATE::stopped
  removes the taskbar entry.
- WEFT_APPD_WS_PORT window global overrides the default port.

New deps: tokio-tungstenite 0.24, futures-util 0.3 (sink+std),
serde_json 1.
2026-03-11 09:01:54 +01:00
b2ba6904c8 test(appd): add dispatch integration tests
5 async tests covering the dispatch function end-to-end:
- dispatch_launch_returns_ack: LaunchApp returns LaunchAck with a
  positive session ID.
- dispatch_terminate_known_returns_stopped: launch then terminate
  returns AppState::Stopped.
- dispatch_terminate_unknown_returns_error: unknown session ID returns
  Error response.
- dispatch_query_running_lists_active_sessions: after two launches,
  QueryRunning returns two session IDs.
- dispatch_query_app_state_returns_starting: newly launched session
  reports AppStateKind::Starting.
2026-03-11 08:40:20 +01:00
6f7adc80c5 test(appd): add unit tests for IPC message codec and session registry
ipc.rs tests (4 tests):
- request_msgpack_roundtrip: LaunchApp serializes and deserializes with
  correct field values.
- response_msgpack_roundtrip: LaunchAck round-trips through MessagePack.
- frame_write_read_roundtrip: write_frame encodes a 4-byte LE length
  header + body; read_frame decodes the framed request correctly.
- read_frame_eof_returns_none: empty stream returns None without error.

main.rs tests (5 tests):
- registry_launch_increments_id: each launch returns a strictly
  increasing session ID.
- registry_terminate_known_session: terminate returns true and state
  transitions to NotFound.
- registry_terminate_unknown_returns_false: terminate on missing ID
  returns false.
- registry_running_ids_reflects_live_sessions: running_ids returns all
  active sessions; terminated sessions are removed.
- registry_state_not_found_for_unknown: querying an unknown session ID
  returns AppStateKind::NotFound.

Also extends scripts/wsl-test.sh to run weft-appd tests alongside
weft-compositor tests.
2026-03-11 08:32:02 +01:00
538eccd4c6 feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.

ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
  Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
  type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
  any AsyncRead / AsyncWrite.

main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
  monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
  UnixListener, sends sd_notify READY=1, then accept-loops with
  ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
  request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
  returns typed Response. Wasmtime spawning and compositor client
  deferred to later implementation.

New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 08:25:55 +01:00
3abc83f9ed test(compositor): add protocol unit tests and stale identifier rejection
Stale identifier rejection (state.rs):
- WeftShellWindowData gains a closed: AtomicBool field (default false).
- Dispatch<ZweftShellWindowV1, WeftShellWindowData>::request() checks the
  closed flag before processing any request; posts a DefunctWindow error
  (code 0) if the window has been closed, satisfying the error enum
  defined in the protocol XML.

Unit tests (protocols/mod.rs, 5 tests):
- window_data_stores_fields: verifies app_id, title, role, and initial
  closed state are stored correctly.
- closed_flag_transition: verifies AtomicBool store/load round-trip.
- manager_interface_name_and_version: confirms generated interface name
  zweft_shell_manager_v1 and version 1.
- window_interface_name_and_version: confirms generated interface name
  zweft_shell_window_v1 and version 1.
- defunct_window_error_code: confirms Error::DefunctWindow == 0 as
  declared in the protocol XML.

Also adds scripts/wsl-test.sh for running cargo test with the
libdisplay-info shim in place.
2026-03-11 08:12:11 +01:00
2bb657e8fc feat(servo-shell): add weft-shell-protocol client-side binding
Generate client-side protocol types from weft-shell-unstable-v1.xml
using wayland-scanner, following the same module structure as the
compositor server side.

- crates/weft-servo-shell/src/protocols/mod.rs: generate_interfaces!
  inside __interfaces submodule, generate_client_code! at client module
  level, with use wayland_client in scope. Re-exports
  ZweftShellManagerV1 and ZweftShellWindowV1 for use by embed_servo
  once the Wayland connection is established.

- New deps: wayland-client, wayland-backend, wayland-scanner, bitflags
  (version-matched to existing workspace resolution).

The binding compiles but is not yet wired into embed_servo(); that
connection is deferred until the Servo embedder contract is ready.
2026-03-11 08:05:03 +01:00
18f92cc341 feat(compositor): implement weft-shell-protocol server side
Add the WEFT compositor-shell Wayland protocol and wire it into the
compositor state.

Protocol definition:
- protocol/weft-shell-unstable-v1.xml: defines zweft_shell_manager_v1
  (global, bound once by servo-shell) and zweft_shell_window_v1
  (per-window slot). Requests: destroy, create_window,
  update_metadata, set_geometry. Events: configure, focus_changed,
  window_closed, presentation_feedback.

Generated code + bindings:
- crates/weft-compositor/src/protocols/mod.rs: uses wayland-scanner
  generate_interfaces! inside a __interfaces sub-module and
  generate_server_code! at the server module level, following the
  wayland-protocols-wlr crate structure. Exports WeftShellState
  (holds the GlobalId) and WeftShellWindowData (per-window user data).

Server-side dispatch (state.rs):
- GlobalDispatch<ZweftShellManagerV1, ()>: binds the global, inits
  each bound resource with unit user data.
- Dispatch<ZweftShellManagerV1, ()>: handles create_window by
  initialising a ZweftShellWindowV1 and sending an initial configure.
- Dispatch<ZweftShellWindowV1, WeftShellWindowData>: handles
  update_metadata (stores advisory data) and set_geometry (echoes
  compositor-adjusted configure back to client).

WeftCompositorState.weft_shell_state initialised in new() alongside
all other protocol globals.

New direct deps in weft-compositor: wayland-scanner, wayland-server,
wayland-backend, bitflags (all version-matched to Smithay 0.7).
2026-03-11 07:59:56 +01:00
c7ad2116a0 feat(appd): add weft-appd skeleton crate and service unit
New crate implementing the application daemon entry point:
- crates/weft-appd/Cargo.toml: tokio (current-thread runtime), anyhow,
  sd-notify, tracing dependencies
- crates/weft-appd/src/main.rs: async run() resolves IPC socket path
  from WEFT_APPD_SOCKET or XDG_RUNTIME_DIR/weft/appd.sock; stubs for
  AppRegistry, IpcServer, CompositorClient, RuntimeSupervisor,
  CapabilityBroker, ResourceController per WEFT-OS-APPD-DESIGN.md;
  sd_notify(READY=1) to be sent after IpcServer bind + CompositorClient
  connect
- infra/systemd/weft-appd.service: Type=notify, Requires+After
  weft-compositor.service, After servo-shell.service

Also fix two winit backend issues that were present in the working tree:
- remove spurious mut on display binding (never mutated after init)
- wrap std::env::set_var in unsafe block (required since Rust 1.80)
2026-03-11 01:13:18 +01:00
43269c9be1 feat(compositor): render pointer cursor from CursorImageStatus::Surface
When a Wayland client calls wl_pointer.set_cursor, render the cursor
surface at pointer_location using render_elements_from_surface_tree.

Changes in render_output:
- Collect output_geo, pointer_location, cursor_status before the inner
 rendering block (avoids borrow conflict with space+drm destructure)
- Build cursor elements as Vec<SpaceRenderElements<_, WaylandSurfaceRenderElement<_>>>
 via render_elements_from_surface_tree with Kind::Cursor; hotspot is
 read from CursorImageSurfaceData on the cursor wl_surface
- Cursor elements prepend space elements (highest z-index first, matching
 render_elements_for_output sort order descending by z_index)
- CursorImageStatus::Hidden / Named: no cursor element emitted

New imports: SpaceRenderElements, CursorImageStatus, CursorImageSurfaceData,
render_elements_from_surface_tree, Kind, Scale, with_states

Hardware cursor plane deferred requires DRM cursor plane API audit.
2026-03-11 00:45:34 +01:00
fc5ada2079 feat(servo-shell): add servo-shell skeleton, system UI, service unit, and Wayland input audit
Includes winit Wayland input audit for servo-shell integration planning.

New files:
- crates/weft-servo-shell/: new workspace member
 - Cargo.toml: anyhow + tracing deps; no servo dep yet (requires git
 dependency on github.com/servo/servo with multi-minute build; deferred
 until embedder contract is confirmed)
 - src/main.rs: reads WAYLAND_DISPLAY and WEFT_SYSTEM_UI_HTML, locates
 system-ui.html from packaged path, calls embed_servo() stub that
 returns a descriptive error explaining the integration work remaining
- infra/shell/system-ui.html: system UI document per blueprint Section 5
 DOM structure (weft-desktop, weft-wallpaper, weft-taskbar, weft-launcher,
 weft-notification-center, weft-window); includes clock and launcher toggle
- infra/systemd/servo-shell.service: Requires+After weft-compositor.service,
 Type=simple, Restart=on-failure
- docs/architecture/winit-wayland-audit.md: audit of winit 0.30.x Wayland
 backend against WEFT input requirements; identifies keyboard shortcut
 inhibit gap, touch gesture gap, IME incomplete (zwp_text_input_v3),
 frame pacing absent (wp_presentation_time), DMA-BUF unverified;
 none block initial integration; all tracked as pre-GA work items

Modified:
- Cargo.toml: add weft-servo-shell to workspace members
- scripts/wsl-check.sh: switch to --workspace for all three gates
2026-03-11 00:34:26 +01:00
61bef1a0a7 feat(compositor): implement surface compositing and layer shell rendering
Replace the clear-colour-only stub in render_output with full
surface compositing via Space::render_elements_for_output.

Changes:
- drm_device.rs: add start_time: Instant to WeftDrmData for elapsed-
 time frame callbacks
- drm.rs: rewrite render_output rendering block
 - collect SpaceRenderElements from Space via render_elements_for_output
 which includes both mapped windows and wlr-layer-shell surfaces from
 layer_map_for_output (sorted by z-index, clipped to output geometry)
 - pass collected elements to DrmOutput::render_frame
 - fix send_frame timing from Duration::ZERO to start_time.elapsed()
 with 16ms throttle hint
 - add space.refresh() and popups.cleanup() after each frame
 Use explicit inner block to scope space+drm borrows so post-render
 bookkeeping can access state mutably

Cursor rendering deferred requires cursor theme loading or MemoryRenderBuffer
setup; tracked separately.
2026-03-11 00:03:33 +01:00
732e572c43 fix(compositor): resolve all Linux cargo check/clippy/fmt failures
Fixes found by running cargo check + clippy -D warnings + fmt --check
on openSUSE Tumbleweed WSL2 (Rust 1.93.0).

input.rs:
- Add GestureBeginEvent (fingers()), GestureEndEvent (cancelled()),
 TouchEvent (slot()) supertrait imports
- Add explicit ::<B> turbofish to all handle_* dispatch calls Rust
 cannot reverse-infer B from an associated type bound
- Remove now-redundant specific gesture/touch event trait imports

state.rs:
- Add reposition_request to XdgShellHandler (E0046)
- Wrap protocol-level LayerSurface in desktop::LayerSurface::new for
 map_layer (E0308)
- Wrap std::env::set_var in unsafe block (E0133, stabilised unsafe in 1.80)
- Add #[allow(dead_code)] on WeftCompositorState protocol state fields
 are held for delegate dispatch, not yet consumed
- Remove spurious mut on display binding

drm.rs:
- Revert initialize_output arg to &output (&Output: Into<OutputModeSource>)
- Specify element type via ::<_, WaylandSurfaceRenderElement<_>> turbofish
 on initialize_output (E0277/E0308)
- Handle Result from scan_connectors, collect via IntoIterator (E0308)
- Wrap set_var in unsafe block; remove spurious mut on display
- Collapse nested if/if-let blocks per clippy::collapsible_if
- Remove useless .into() on render_node (clippy::useless_conversion)

drm_device.rs:
- Add #[allow(dead_code)] on WeftOutputSurface (device_id, global used
 in hotplug handling)

scripts/wsl-check.sh (new):
- WSL2 helper: injects libdisplay-info 0.2.9 shim .pc to satisfy the
 < 0.3.0 constraint while openSUSE ships 0.3.0; runs check/clippy/fmt
2026-03-10 23:43:43 +01:00
4d21b8dba0 fix(compositor): pass Output by value to initialize_output
From<Output> for OutputModeSource is more reliably implemented than
From<&Output>, avoiding a potential type mismatch on Linux CI.
2026-03-10 22:37:26 +01:00
8925ebe3df feat(compositor): implement DRM/KMS rendering path
Add full DRM/KMS backend with libseat session, GBM allocation, EGL
display initialisation, and a GpuManager-driven rendering loop.

- drm_device.rs: type aliases and per-device/per-output state structs
 (WeftDrmDevice, WeftOutputSurface, WeftDrmData)
- drm.rs: replace skeleton with complete backend libseat session,
 udev device enumeration, libinput event source, connector scanning
 via smithay-drm-extras DrmScanner, DrmOutputManager initialisation
 per CRTC, VBlank-driven render_output, sd_notify(READY=1)
- state.rs: add drm: Option<WeftDrmData> field; route dmabuf import
 through GPU manager when the DRM path is active
- Cargo.toml: add renderer_multi, use_system_lib Smithay features;
 add smithay-drm-extras and sd-notify Linux dependencies

render_output submits a clear-colour-only frame to establish
the VBlank pipeline. Surface compositing is wired up in a subsequent commit.
2026-03-10 22:32:21 +01:00
767da43047 fix(compositor): pass owned surface to keyboard.set_focus, remove redundant clone 2026-03-10 21:31:59 +01:00
ae1d572803 fix(compositor): use as_deref() for WaylandFocus::wl_surface() comparisons 2026-03-10 21:28:55 +01:00
c90679ea7b fix(compositor): remove nonexistent PresentationHandler trait impl 2026-03-10 21:27:02 +01:00
fcd4a3bacd style(compositor): apply rustfmt formatting 2026-03-10 21:24:23 +01:00
e981044c43 fix(compositor): add missing BufferHandler supertrait impl for DmabufHandler 2026-03-10 21:23:33 +01:00
3ebe50ef31 fix(compositor): correct remaining Smithay 0.7 API issues
- Remove nonexistent TextInputHandler trait impl
- Add missing cursor_position_hint to PointerConstraintsHandler
- Rewrite winit/drm backends: ListeningSocketSource + insert_client
 for per-client WeftClientState; Generic<Display<D>> for dispatch
- Remove calloop direct dep; use via smithay::reexports::calloop
- Remove unused imports in input.rs
2026-03-10 21:20:28 +01:00
f70a998f67 fix(compositor): correct Smithay 0.7 API usage
- Fix smithay feature name: renderer_gles -> renderer_gl
- Rewrite winit backend: WinitEventLoop as calloop source,
 render_output free function, bind() returning (renderer, framebuffer),
 socket creation via add_socket_auto, correct WinitEvent::Redraw variant
- Fix InputMethodHandler: add dismiss_popup, remove nonexistent popup_done,
 use smithay::wayland::input_method::PopupSurface not XDG variant
- Remove nonexistent CursorShapeHandler trait; add TabletSeatHandler impl
 (required bound for delegate_cursor_shape!)
- Add state.running bool; remove LoopSignal::is_stopped() call (does not exist)
- Remove unused direct deps: calloop-wayland-source, wayland-protocols,
 wayland-protocols-wlr
- Split CI into cross-platform and linux-only jobs; install Wayland system
 dependencies in linux-only job
2026-03-10 21:09:54 +01:00
feb69be199 feat(compositor): add weft-compositor crate
- Implement WeftCompositorState with all Wayland protocol globals:
 compositor, xdg-shell, layer-shell, shm, dmabuf, output, presentation,
 text-input, input-method, pointer-constraints, cursor-shape, seat.
- Implement process_input_event covering keyboard, pointer (relative +
 absolute), axis, touch, and all gesture types (swipe, pinch, hold).
- Implement Winit backend with damage-tracked rendering loop and frame
 callbacks.
- Add DRM/KMS backend skeleton: libseat session, udev device discovery,
 calloop integration (rendering path deferred).
- Add infra/systemd/weft-compositor.service (Type=notify).
- Split CI into cross-platform and linux-only jobs.
- Exclude weft-compositor from Windows check scripts.
2026-03-10 20:56:35 +01:00
8b65f81396 Initialize Rust workspace and repository metadata 2026-03-10 18:47:06 +01:00