diff --git a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md index ce65d927..0cbb0d6b 100644 --- a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md +++ b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md @@ -1,8 +1,8 @@ # Aether Events Launcher: Native Electron Integration -> **Status:** Operational / Phase 5 Implementation -> **Last Updated:** February 10, 2026 -> **Primary Platform:** macOS (Darwin) +> **Status:** Operational / Phase 5 Implementation +> **Last Updated:** 2026-03-11 +> **Primary Platform:** macOS (Darwin) > **Fallback Platform:** Linux / Windows ## 1. Overview @@ -22,7 +22,8 @@ The Aether Events Launcher utilizes an Electron-based "Native Shell" to provide The integration is built on a decoupled three-layer communication model to ensure security and cross-platform flexibility. ### 2.1 Layer 1: The Engine (Main Process) -- **File:** `aether_app_native/src/main/*.ts` +- **Repo:** `~/OSIT_dev/aether_app_native_electron/` (separate git repo) +- **File:** `aether_app_native_electron/src/main/*.ts` - **Role:** Performs the heavy lifting (Filesystem, Shell, AppleScript). - **Responsibilities:** - Managing the **Hashed Cache** directory. @@ -94,9 +95,14 @@ The native shell provides specialized handlers for controlling the "Podium Exper - **Telemetry:** Pushes `cpu_usage`, `memory_free_gb`, and `foreground_app` via heartbeats using the `get_device_info` relay. - **Self-Update (Roadmap):** Plan to monitor Syncthing `admin_share` for newer `.app` versions and perform atomic swaps. -### 5.3 Planned Actuators (Future Phases) -- **Recording:** Control for `aperture` session capture (`start`, `stop`). -- **Display Layouts:** Toggling between Mirror and Extended modes via `displayplacer`. +### 5.3 Implemented Actuators (Phase 5 Complete) +- **Recording:** `manage_recording({action})` — Aperture session capture (`start`, `stop`, `status`). macOS only. +- **Display Layouts:** `set_display_layout({mode})` — Mirror / Extend via `displayplacer`. macOS only. +- **Power Control:** `power_control({action})` — Shutdown, reboot, sleep. macOS + Linux. +- **Window Control:** `window_control({action})` — Maximize, minimize, fullscreen, kiosk mode. +- **Wallpaper:** `set_wallpaper({path})` — macOS (AppleScript) + Linux (gsettings). + +> **Note:** `update_app` is implemented as a stub — downloads but does not install. Not yet functional for end users. --- @@ -128,19 +134,47 @@ The UI dynamically filters fields based on the user's focus. Enabling Technical ## 7. Implementation Reference (IPC Whitelist) -### Core Functions (`electron_relay.ts`) -- `get_device_config()`: Returns hydrated device settings from the native shell. -- `get_device_info()`: Returns OS metadata, IP list, and path placeholders (`[home]`, `[tmp]`). -- `check_hash_file_cache({cache_root, hash})`: Local filesystem hash check. -- `download_to_cache({url, cache_root, hash, ...})`: Authorized background download to the hashed cache. -- `launch_from_cache({cache_root, hash, temp_root, filename})`: Atomic "Safe Handover" trigger. -- `launch_presentation({path, app})`: Phase 5 specialized launcher with slideshow activation. -- `control_presentation({app, action})`: Remote navigation (next/prev) for active decks. -- `kill_processes({process_name_li})`: Graceful application termination. -- `manage_recording({action})`: Presentation session capture control (Aperture). -- `list_tools()`: Manifest of all available native bridge functions. +All functions below are exported from `src/lib/electron/electron_relay.ts` and safely +no-op when `window.aetherNative` is not present (i.e., in browser/non-native mode). + +### Config & Info +- `get_device_config()` — Returns hydrated device settings injected by the native shell on startup. +- `get_device_info()` — Returns OS metadata, IP list, hostname, and path placeholders (`[home]`, `[tmp]`). + +### File Cache +- `check_hash_file_cache({cache_root, hash, hash_prefix_length?})` — Verifies a file exists in the local hashed cache. +- `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check. +- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?})` — Atomic "Safe Handover": copy from cache → tmp → rename → execute. +- `cleanup_tmp_files({cache_root, max_age_minutes?})` — Removes stale `*.tmp` download artifacts. Default: 1440 min (24h). Called at launcher startup. + +> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories. + +### Shell & OS +- `open_folder(path)` — Opens a path in the OS file manager. +- `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution. +- `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution. +- `run_osascript(script)` — Executes an AppleScript string. macOS only. +- `kill_processes({process_name_li})` — Gracefully terminates processes by name. +- `open_local_file_v2(path)` — Opens a file with its default OS application. + +### Presentations (Phase 5) +- `launch_presentation({path, app?, os?})` — Platform-aware launcher. macOS: PowerPoint/Keynote via AppleScript. Linux: LibreOffice Impress. Resolves `[home]`/`[tmp]` placeholders. +- `control_presentation({app, action})` — Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript. + +### System Management (Phase 5) +- `set_wallpaper({path})` — Sets desktop wallpaper. macOS (AppleScript) + Linux (gsettings). +- `window_control({action, value?})` — Electron window management: maximize, minimize, fullscreen, kiosk. +- `set_display_layout({mode, configStr?})` — Mirror or extend displays via displayplacer. macOS only. +- `power_control({action})` — Shutdown, reboot, or sleep the host machine. macOS + Linux. +- `manage_recording({action, options?})` — Aperture capture control (`start`/`stop`/`status`). macOS only. +- `open_external({url, app?})` — Opens a URL in Chrome, Firefox, or the default browser. +- `update_app(args)` — **Stub only.** Downloads but does not install. Not yet functional. +- `list_tools()` — Returns a self-documenting manifest of all available native bridge functions. ### Path Placeholders -To ensure cross-platform compatibility, all paths should use: -- `[home]`: User home directory. -- `[tmp]`: System temporary directory. +All paths passed to native handlers should use tokens rather than hardcoded OS paths: +- `[home]` — Resolved to the user's home directory by the native bridge. +- `[tmp]` — Resolved to the system temporary directory. + +### Not Exposed via Relay (intentional) +- `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly. diff --git a/src/lib/electron/README.md b/src/lib/electron/README.md index 23abe00a..ca1f55bb 100644 --- a/src/lib/electron/README.md +++ b/src/lib/electron/README.md @@ -1,34 +1,50 @@ # Aether Native Integration (Electron) -This directory contains the SvelteKit-side bridge for the Aether Native App (Electron). It provides the UI with access to OS-level capabilities required for the **Event Launcher**. +This directory contains the SvelteKit-side bridge for the Aether Native App (Electron). +It provides the UI with access to OS-level capabilities required for the **Event Launcher**. -## 📖 Technical Manual -For detailed architecture, lifecycle, and IPC specifications, see: -👉 **[AE_EVENTS_LAUNCHER_NATIVE_INTEGRATION.md](../../../documentation/AE_EVENTS_LAUNCHER_NATIVE_INTEGRATION.md)** +The native implementation lives in a separate repo: +`~/OSIT_dev/aether_app_native_electron/` -## 📂 File Manifest +## Technical Manual + +For detailed architecture, lifecycle, and IPC specifications, see: +[PROJECT__AE_Events_Launcher_Native_integration.md](../../../documentation/PROJECT__AE_Events_Launcher_Native_integration.md) + +## File Manifest | File | Role | Description | | :--- | :--- | :--- | -| `electron_relay.ts` | **Messenger** | The TypeScript API used by Svelte components. Standardizes calls to `snake_case`. | -| `electron_native.js` | **Bridge Logic** | (Internal) Logic often used in the Preload or Renderer to facilitate IPC. | +| `electron_relay.ts` | **Active — Messenger** | The TypeScript API used by Svelte components. Standardizes all IPC calls to `snake_case`. This is the only file in this directory that should be imported. | +| `electron_native.js` | **DEPRECATED — Do not import** | Legacy V2/V4 reference file. The active native logic lives in `aether_app_native_electron/`. Kept for historical reference only. | -## 🚀 Usage Example +## Usage Example ```typescript import { is_native, launch_from_cache } from '$lib/electron/electron_relay'; if (is_native) { await launch_from_cache({ - cache_root: $ae_loc.native_device.local_file_cache_path, + cache_root: $ae_loc.local_file_cache_path, hash: file_obj.hash_sha256, - temp_root: $ae_loc.native_device.host_file_temp_path, + temp_root: $ae_loc.host_file_temp_path, filename: file_obj.filename }); } ``` -## 🔐 Security Standards -- **Namespace:** All native functions are exposed via `window.aetherNative`. -- **Whitelisting:** Only specific intents (e.g., `launch_presentation`) are allowed. -- **Path Isolation:** All file operations are restricted to `[home]` and `[tmp]` via placeholder resolution. +## Security Model + +- All native functions are accessed via `window.aetherNative` (contextBridge) +- Only explicitly whitelisted IPC channels are exposed — no arbitrary code execution +- Path isolation: all file operations use `[home]` / `[tmp]` placeholders resolved by the native bridge +- Context isolation is enabled; Node integration is disabled in the renderer + +## Bridge Architecture (Three Layers) + +```text +Svelte Component + └── electron_relay.ts (this directory — typed wrappers, snake_case API) + └── window.aetherNative (contextBridge whitelist in preload/index.ts) + └── ipcMain handlers (aether_app_native_electron/src/main/*.ts) +``` diff --git a/src/lib/electron/tmp_shell_handlers.ts b/src/lib/electron/tmp_shell_handlers.ts deleted file mode 100644 index ead12b6b..00000000 --- a/src/lib/electron/tmp_shell_handlers.ts +++ /dev/null @@ -1,212 +0,0 @@ -// @ts-nocheck -import { ipcMain, shell } from 'electron'; -import { exec, execSync } from 'child_process'; -import * as fs from 'fs'; -import * as os from 'os'; -import { expandPath } from './file_utils'; - -export function registerShellHandlers() { - ipcMain.handle('native:open-folder', async (event, folderPath: string) => { - const cleanPath = expandPath(folderPath); - const error = await shell.openPath(cleanPath); - return { success: !error, error }; - }); - - ipcMain.handle('native:run-cmd', async (event, { cmd, timeout = 30000 }) => { - const cleanCmd = expandPath(cmd); - return new Promise((resolve) => { - exec(cleanCmd, { timeout }, (error, stdout, stderr) => { - resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null }); - }); - }); - }); - - ipcMain.handle('native:run-cmd-sync', async (event, { cmd }) => { - const cleanCmd = expandPath(cmd); - try { - const stdout = execSync(cleanCmd).toString(); - return { success: true, stdout: stdout.trim() }; - } catch (error: any) { - return { success: false, error: error.message, stderr: error.stderr?.toString() }; - } - }); - - ipcMain.handle('native:run-osascript', async (event, script: string) => { - if (os.platform() !== 'darwin') return { success: false, error: 'AppleScript is only available on macOS' }; - const escapedScript = script.replace(/"/g, '\"'); - const cmd = `osascript -e "${escapedScript}"`; - return new Promise((resolve) => { - exec(cmd, (error, stdout, stderr) => { - resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null }); - }); - }); - }); - - ipcMain.handle('native:kill-processes', async (event, { process_name_li = [] }) => { - console.log(`Native: Killing processes -> `, process_name_li); - const results = []; - for (const name of process_name_li) { - const cmd = os.platform() === 'win32' - ? `taskkill /F /IM ${name} /T` - : `pkill -f ${name}`; - try { - execSync(cmd); - results.push({ name, success: true }); - } catch (e: any) { - results.push({ name, success: false, error: e.message }); - } - } - return { success: true, results }; - }); - - ipcMain.handle('native:open-local-file-v2', async (event, filePath: string) => { - const cleanPath = expandPath(filePath); - const error = await shell.openPath(cleanPath); - return { success: !error, error }; - }); - - ipcMain.handle('native:launch-presentation', async (event, { path: rawPath, app: appType = 'default' }) => { - const cleanedPath = expandPath(rawPath); - console.log(`Native: Launching Presentation -> ${cleanedPath} (App: ${appType})`); - - if (os.platform() === 'linux') { - const cmd = `libreoffice --impress "${cleanedPath}"`; - return new Promise((resolve) => { - exec(cmd, (err, stdout, stderr) => { - if (err) resolve({ success: false, error: err.message }); - else resolve({ success: true, stdout, stderr }); - }); - }); - } - - if (os.platform() === 'darwin') { - let script = ''; - if (appType === 'keynote') { - script = ` - tell application "Keynote" - activate - open (POSIX file "${cleanedPath}") - delay 1 - start (front document) - end tell - `; - } else if (appType === 'powerpoint') { - script = ` - tell application "Microsoft PowerPoint" - activate - open (POSIX file "${cleanedPath}") - delay 1 - run slide show of active presentation - end tell - `; - } - - if (script) { - return new Promise((resolve) => { - const escapedScript = script.replace(/"/g, '\\"'); - exec(`osascript -e "${escapedScript}"`, (err, stdout, stderr) => { - if (err) resolve({ success: false, error: err.message }); - else resolve({ success: true }); - }); - }); - } - } - - const error = await shell.openPath(cleanedPath); - return { success: !error, error }; - }); - - ipcMain.handle('native:control-presentation', async (event, { app, action }) => { - if (os.platform() !== 'darwin') return { success: false, error: 'Presentation control is only available on macOS' }; - - let script = ''; - if (app === 'powerpoint') { - switch (action) { - case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break; - case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break; - case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break; - case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break; - } - } else if (app === 'keynote') { - switch (action) { - case 'next': script = 'tell application "Keynote" to show next'; break; - case 'prev': script = 'tell application "Keynote" to show previous'; break; - case 'start': script = 'tell application "Keynote" to start (front document)'; break; - case 'stop': script = 'tell application "Keynote" to stop'; break; - } - } - - if (!script) return { success: false, error: `Unsupported app or action: ${app}/${action}` }; - - return new Promise((resolve) => { - exec(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => { - resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null }); - }); - }); - }); - - ipcMain.handle('native:list-tools', async () => { - return [ - { - name: 'open_folder', - description: 'Opens a directory in the OS file explorer (Finder/Files/Explorer).', - params: { path: 'string' } - }, - { - name: 'run_cmd', - description: 'Executes an asynchronous shell command with a timeout.', - params: { cmd: 'string', timeout: 'number (optional)' } - }, - { - name: 'run_cmd_sync', - description: 'Executes a synchronous shell command.', - params: { cmd: 'string' } - }, - { - name: 'run_osascript', - description: 'Executes a raw AppleScript string (macOS only).', - params: { script: 'string' } - }, - { - name: 'kill_processes', - description: 'Forcefully terminates processes by name.', - params: { process_name_li: 'string[]' } - }, - { - name: 'open_local_file_v2', - description: 'Opens a local file using the default OS handler.', - params: { filePath: 'string' } - }, - { - name: 'launch_presentation', - description: 'Phase 5: Specialized launcher for PowerPoint, Keynote, and LibreOffice with auto-focus.', - params: { path: 'string', app: 'default|powerpoint|keynote' } - }, - { - name: 'control_presentation', - description: 'Phase 5: Remote navigation for active slideshows.', - params: { app: 'powerpoint|keynote', action: 'next|prev|start|stop' } - }, - { - name: 'check_cache', - description: 'Checks if a file exists in the local organized cache.', - params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number' } - }, - { - name: 'download_to_cache', - description: 'Downloads a file from the API directly into the native cache.', - params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string' } - }, - { - name: 'launch_from_cache', - description: 'Atomic operation: Copies file from cache to temp with original name and launches via specialized handler.', - params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string' } - }, - { - name: 'get_device_info', - description: 'Returns hardware and OS metadata (CPUs, RAM, IP addresses, Hostname).', - params: {} - } - ]; - }); -}