Fix local reset flow and Accessibility permission handling (#324)

* refactor: clean up imports and improve code structure in skills_cli and registry_ops

- Removed unused imports in `skills_cli.rs` and `registry_ops.rs` to enhance code clarity.
- Streamlined import statements for better organization and readability.
- Minor adjustments in `engine.rs` to maintain consistency in import usage.

* fix: update UI text and styles for MemoryWorkspace and SkillSetupWizard components

- Changed the title in MemoryWorkspace to "Memory (EverMind)" for clarity.
- Updated text colors in SkillSetupWizard for better visibility and consistency.
- Enhanced button styles and loading indicators for improved user experience.

* feat: implement reset functionality for OpenHuman data and enhance app data clearing process

- Added a new utility function `resetOpenHumanDataAndRestartCore` to reset local OpenHuman data and restart the core process.
- Updated `clearAllAppData` in `SettingsHome` to utilize the new reset function, improving the app's data clearing process.
- Enhanced error handling during the data reset and session clearing operations to ensure robustness.
- Introduced tests for the new reset functionality to validate its behavior and integration.

* refactor: remove screen recording permission from AccessibilityPanel

- Eliminated the screen recording permission check and associated UI elements from the AccessibilityPanel component.
- Updated tests to ensure the screen recording option is no longer present in the rendered output, improving clarity and focus on relevant permissions.

* chore: update package.json files to integrate Husky for Git hooks

- Added Husky as a dev dependency in both package.json files to manage Git hooks.
- Included "prepare" and "postinstall" scripts in the main package.json to ensure Husky is set up correctly.
- Cleaned up the app's package.json by removing the "prepare" script, as it is now handled in the main package.json.
- Enhanced logging in tauriCommands.ts for better debugging during the reset process.

* fix: update variable name for clarity in ops.rs test assertions

- Changed the variable name from `data` to `value` in the test assertions to enhance clarity and better reflect its purpose.
- Ensured that the assertions remain functional and maintain the integrity of the test logic.

* feat: enhance skill selection logic in skills_debug_e2e.rs

- Introduced a new function `select_skill_id` to improve skill selection based on environment variables and preferred skills.
- Updated the skill ID retrieval process to prioritize user-defined skills while maintaining a fallback to the default skill.
- Enhanced documentation to clarify the usage of environment variables for skill testing.

* format
This commit is contained in:
Steven Enamakel
2026-04-04 20:26:08 -07:00
committed by GitHub
parent fa1c618ca9
commit e925e8a319
20 changed files with 279 additions and 75 deletions
+1 -2
View File
@@ -40,8 +40,7 @@
"format": "prettier --write . && cargo fmt --manifest-path ../Cargo.toml --all",
"format:check": "prettier --check . && cargo fmt --manifest-path ../Cargo.toml --all --check",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"prepare": "husky"
"lint:fix": "eslint . --ext .ts,.tsx --fix"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
+5 -7
View File
@@ -208,7 +208,10 @@ impl CoreProcessHandle {
);
self.shutdown().await;
log::debug!("[core] restart: shutdown complete, checking port {}", self.port);
log::debug!(
"[core] restart: shutdown complete, checking port {}",
self.port
);
// If we never spawned the sidecar (something else was already listening), we cannot free
// the port — fail fast with a clear message instead of polling for 8s.
@@ -265,12 +268,7 @@ impl CoreProcessHandle {
}
// Wait for the process to exit so the RPC listen socket is released before restart
// checks the port (otherwise we can spuriously hit "port still in use").
match timeout(
Duration::from_secs(12),
child.wait(),
)
.await
{
match timeout(Duration::from_secs(12), child.wait()).await {
Ok(Ok(status)) => {
log::debug!("[core] child core process reaped after kill: {status}");
}
+4 -5
View File
@@ -143,10 +143,7 @@ async fn restart_core_process(
/// Register (or re-register) the global dictation toggle hotkey.
/// Emits `dictation://toggle` to all webviews when the shortcut is pressed.
#[tauri::command]
async fn register_dictation_hotkey(
app: AppHandle,
shortcut: String,
) -> Result<(), String> {
async fn register_dictation_hotkey(app: AppHandle, shortcut: String) -> Result<(), String> {
log::info!("[dictation] register_dictation_hotkey: shortcut={shortcut}");
let old_shortcuts = {
@@ -189,7 +186,9 @@ async fn register_dictation_hotkey(
);
}
}
return Err(format!("Failed to unregister previous shortcut '{old}': {e}"));
return Err(format!(
"Failed to unregister previous shortcut '{old}': {e}"
));
}
unregistered_old.push(old.clone());
}
@@ -350,7 +350,7 @@ export function MemoryWorkspace({ onToast }: MemoryWorkspaceProps) {
<div className="glass rounded-2xl p-5 border border-stone-200">
<div className="flex items-start justify-between gap-4 mb-5">
<div>
<h2 className="text-lg font-semibold text-stone-900">Memory</h2>
<h2 className="text-lg font-semibold text-stone-900">Memory (EverMind)</h2>
<p className="text-sm text-stone-500">
Your AI's knowledge graph, extracted insights, and ingestion activity.
</p>
+13 -10
View File
@@ -3,6 +3,7 @@ import { useState } from 'react';
import { skillManager } from '../../lib/skills/manager';
import { useCoreState } from '../../providers/CoreStateProvider';
import { persistor } from '../../store';
import { resetOpenHumanDataAndRestartCore } from '../../utils/tauriCommands';
import SettingsHeader from './components/SettingsHeader';
import SettingsMenuItem from './components/SettingsMenuItem';
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
@@ -29,29 +30,31 @@ const SettingsHome = () => {
};
const clearAllAppData = async () => {
try {
await setOnboardingCompletedFlag(false);
} catch (err) {
console.warn('[Settings] Failed to clear onboarding_completed in config:', err);
}
try {
await clearSession();
} catch (err) {
console.warn('[Settings] Rust logout failed during clearAllAppData:', err);
}
await persistor.purge();
window.localStorage.clear();
window.sessionStorage.clear();
try {
await resetOpenHumanDataAndRestartCore();
} catch (err) {
console.warn('[Settings] Failed to reset OpenHuman data dir and restart core:', err);
throw err;
}
// NEW: Clear skills databases (emails, chats, cached files)
// Best-effort cleanup for in-memory and browser-side caches that live outside the Rust core.
try {
await skillManager.clearAllSkillsData();
} catch (error) {
console.warn('Failed to clear skills data:', error);
// Continue with logout even if skills clearing fails
// Continue even if skill cleanup fails because the backend reset already completed.
}
await persistor.purge();
window.localStorage.clear();
window.sessionStorage.clear();
// Complete reset - redirect to login for fresh start
window.location.hash = '/';
};
@@ -85,7 +85,6 @@ const AccessibilityPanel = () => {
}, [dispatch]);
const anyPermissionDenied =
status?.permissions.screen_recording === 'denied' ||
status?.permissions.accessibility === 'denied' ||
status?.permissions.input_monitoring === 'denied';
@@ -119,10 +118,6 @@ const AccessibilityPanel = () => {
<div className="flex-1 overflow-y-auto max-w-2xl mx-auto w-full p-4 space-y-4">
<section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900">Permissions</h3>
<PermissionBadge
label="Screen Recording"
value={status?.permissions.screen_recording ?? 'unknown'}
/>
<PermissionBadge
label="Accessibility"
value={status?.permissions.accessibility ?? 'unknown'}
@@ -154,18 +149,11 @@ const AccessibilityPanel = () => {
</div>
)}
<button
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('screen_recording'))}
disabled={isRequestingPermissions || isRestartingCore}
className="mt-1 rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Request Screen Recording'}
</button>
<button
type="button"
onClick={() => void dispatch(requestAccessibilityPermission('accessibility'))}
disabled={isRequestingPermissions || isRestartingCore}
className="rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
className="mt-1 rounded-lg border border-primary-500/60 bg-primary-50 px-3 py-2 text-sm text-primary-600 disabled:opacity-50">
{isRequestingPermissions ? 'Requesting…' : 'Request Accessibility'}
</button>
<button
@@ -95,6 +95,7 @@ describe('AccessibilityPanel', () => {
expect(screen.getByText('Accessibility Automation')).toBeInTheDocument();
expect(screen.getByText('Permissions')).toBeInTheDocument();
expect(screen.getByText('Session')).toBeInTheDocument();
expect(screen.queryByText('Screen Recording')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Start Session' })).toBeInTheDocument();
});
});
+16 -16
View File
@@ -503,11 +503,11 @@ export default function SkillSetupWizard({
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">
<h3 className="text-lg font-semibold text-stone-900 mb-2">
Connected!
</h3>
{state.message && (
<p className="text-sm text-stone-400 mb-6">{state.message}</p>
<p className="text-sm text-stone-600 mb-6">{state.message}</p>
)}
<button
onClick={onComplete}
@@ -536,14 +536,14 @@ export default function SkillSetupWizard({
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">
<h3 className="text-lg font-semibold text-stone-900 mb-2">
Setup Failed
</h3>
<p className="text-sm text-stone-400 mb-6">{state.message}</p>
<p className="text-sm text-stone-600 mb-6">{state.message}</p>
<div className="flex space-x-3 justify-center">
<button
onClick={handleCancel}
className="px-6 py-2.5 text-sm font-medium text-stone-400 bg-stone-800/50 border border-stone-700 rounded-xl hover:bg-stone-800 transition-colors"
className="px-6 py-2.5 text-sm font-medium text-stone-600 bg-stone-100 border border-stone-200 rounded-xl hover:bg-stone-200 transition-colors"
>
Close
</button>
@@ -621,7 +621,7 @@ function LoadingView() {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span className="ml-3 text-sm text-stone-400">
<span className="ml-3 text-sm text-stone-600">
Starting setup...
</span>
</div>
@@ -664,17 +664,17 @@ function OAuthLoginView({
<div className="py-6">
{/* Provider icon */}
<div className="flex justify-center mb-5">
<div className="w-14 h-14 rounded-2xl bg-stone-800 border border-stone-700 flex items-center justify-center">
<div className="w-14 h-14 rounded-2xl bg-stone-50 border border-stone-200 flex items-center justify-center shadow-sm">
<ProviderIcon provider={provider} />
</div>
</div>
{/* Title and description */}
<div className="text-center mb-6">
<h3 className="text-lg font-semibold text-white mb-2">
<h3 className="text-lg font-semibold text-stone-900 mb-2">
Connect to {providerName}
</h3>
<p className="text-sm text-stone-400">
<p className="text-sm text-stone-600">
{waiting
? "Waiting for authorization. Complete the login in your browser..."
: `Sign in with your ${providerName} account to connect this skill.`}
@@ -684,9 +684,9 @@ function OAuthLoginView({
{/* Login button or waiting state */}
{waiting ? (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-3 px-4 py-3 bg-stone-800/50 border border-stone-700 rounded-xl">
<div className="flex items-center gap-3 px-4 py-3 bg-stone-50 border border-stone-200 rounded-xl">
<svg
className="animate-spin h-4 w-4 text-primary-400"
className="animate-spin h-4 w-4 text-primary-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -705,14 +705,14 @@ function OAuthLoginView({
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span className="text-sm text-stone-300">
<span className="text-sm text-stone-700">
Waiting for {providerName} authorization...
</span>
</div>
<button
onClick={onLogin}
className="text-xs text-primary-400 hover:text-primary-300 transition-colors"
className="text-xs text-primary-600 hover:text-primary-700 transition-colors"
>
Open login page again
</button>
@@ -743,7 +743,7 @@ function OAuthLoginView({
<div className="mt-4">
<button
onClick={onCancel}
className="w-full py-2.5 text-sm font-medium text-stone-400 bg-stone-800/50 border border-stone-700 rounded-xl hover:bg-stone-800 transition-colors"
className="w-full py-2.5 text-sm font-medium text-stone-600 bg-stone-100 border border-stone-200 rounded-xl hover:bg-stone-200 transition-colors"
>
Cancel
</button>
@@ -796,7 +796,7 @@ function ProviderIcon({ provider }: { provider: string }) {
);
case "github":
return (
<svg className="w-7 h-7 text-white" fill="currentColor" viewBox="0 0 24 24">
<svg className="w-7 h-7 text-stone-800" fill="currentColor" viewBox="0 0 24 24">
<path
fillRule="evenodd"
clipRule="evenodd"
@@ -807,7 +807,7 @@ function ProviderIcon({ provider }: { provider: string }) {
default:
return (
<svg
className="w-7 h-7 text-stone-400"
className="w-7 h-7 text-stone-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
+11 -1
View File
@@ -1,4 +1,4 @@
import { isTauri } from '@tauri-apps/api/core';
import { invoke, isTauri } from '@tauri-apps/api/core';
import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest';
import { callCoreRpc } from '../../services/coreRpcClient';
@@ -8,8 +8,10 @@ vi.mock('../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
describe('tauriCommands', () => {
const mockIsTauri = isTauri as Mock;
const mockInvoke = invoke as Mock;
const mockCallCoreRpc = callCoreRpc as Mock;
let getAuthState: typeof import('../tauriCommands').getAuthState;
let resetOpenHumanDataAndRestartCore: typeof import('../tauriCommands').resetOpenHumanDataAndRestartCore;
let storeSession: typeof import('../tauriCommands').storeSession;
let openhumanLocalAiStatus: typeof import('../tauriCommands').openhumanLocalAiStatus;
let openhumanServiceStatus: typeof import('../tauriCommands').openhumanServiceStatus;
@@ -19,6 +21,7 @@ describe('tauriCommands', () => {
mockIsTauri.mockReturnValue(true);
const actual = await vi.importActual<typeof import('../tauriCommands')>('../tauriCommands');
getAuthState = actual.getAuthState;
resetOpenHumanDataAndRestartCore = actual.resetOpenHumanDataAndRestartCore;
storeSession = actual.storeSession;
openhumanLocalAiStatus = actual.openhumanLocalAiStatus;
openhumanServiceStatus = actual.openhumanServiceStatus;
@@ -44,6 +47,13 @@ describe('tauriCommands', () => {
});
});
test('resetOpenHumanDataAndRestartCore invokes the destructive Tauri command', async () => {
await resetOpenHumanDataAndRestartCore();
expect(mockCallCoreRpc).toHaveBeenCalledWith({ method: 'openhuman.config_reset_local_data' });
expect(mockInvoke).toHaveBeenCalledWith('restart_core_process');
});
test('openhumanLocalAiStatus returns upgrade hint on unknown method', async () => {
mockCallCoreRpc.mockRejectedValueOnce(new Error('unknown method: openhuman.local_ai_status'));
+16
View File
@@ -200,6 +200,22 @@ export async function restartCoreProcess(): Promise<void> {
console.debug('[core] restartCoreProcess: done');
}
export async function resetOpenHumanDataAndRestartCore(): Promise<void> {
if (!isTauri()) {
console.debug('[core] resetOpenHumanDataAndRestartCore: skipped — not running in Tauri');
return;
}
console.debug(
'[core] resetOpenHumanDataAndRestartCore: invoking openhuman.config_reset_local_data'
);
await callCoreRpc({ method: 'openhuman.config_reset_local_data' });
console.debug(
'[core] resetOpenHumanDataAndRestartCore: local data reset complete, restarting core'
);
await restartCoreProcess();
console.debug('[core] resetOpenHumanDataAndRestartCore: done');
}
// --- Memory Commands ---
/**
+5
View File
@@ -17,6 +17,8 @@
"format:check": "yarn workspace openhuman-app format:check",
"lint": "yarn workspace openhuman-app lint",
"lint:fix": "yarn workspace openhuman-app lint:fix",
"prepare": "husky",
"postinstall": "husky",
"tauri": "yarn workspace openhuman-app tauri",
"test": "yarn workspace openhuman-app test",
"test:coverage": "yarn workspace openhuman-app test:coverage",
@@ -24,6 +26,9 @@
"mock:api": "node scripts/mock-api-server.mjs",
"typecheck": "yarn workspace openhuman-app compile"
},
"devDependencies": {
"husky": "^9.1.7"
},
"dependencies": {
"@tauri-apps/api": "2.10.1"
}
-2
View File
@@ -417,9 +417,7 @@ fn run_skills_test(args: &[String]) -> Result<()> {
// ---------------------------------------------------------------------------
fn build_skills_only_router() -> axum::Router {
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::Json;
axum::Router::new()
.route("/health", get(health))
+124 -1
View File
@@ -1,6 +1,6 @@
//! JSON-RPC / CLI controller surface for persisted config and runtime flags.
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use serde::Serialize;
use serde_json::json;
@@ -39,6 +39,86 @@ fn fallback_workspace_dir() -> PathBuf {
.join("workspace")
}
fn default_openhuman_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".openhuman")
}
fn active_workspace_marker_path(default_openhuman_dir: &Path) -> PathBuf {
default_openhuman_dir.join("active_workspace.toml")
}
fn config_openhuman_dir(config: &Config) -> PathBuf {
config
.config_path
.parent()
.map_or_else(|| PathBuf::from("."), PathBuf::from)
}
async fn reset_local_data_for_paths(
current_openhuman_dir: &Path,
default_openhuman_dir: &Path,
) -> Result<RpcOutcome<serde_json::Value>, String> {
let active_workspace_marker = active_workspace_marker_path(default_openhuman_dir);
tracing::debug!(
current_dir = %current_openhuman_dir.display(),
default_dir = %default_openhuman_dir.display(),
marker = %active_workspace_marker.display(),
"[config] reset_local_data: starting"
);
let mut removed_paths = Vec::new();
if active_workspace_marker.exists() {
tokio::fs::remove_file(&active_workspace_marker)
.await
.map_err(|e| format!("Failed to remove active workspace marker: {e}"))?;
tracing::debug!(
marker = %active_workspace_marker.display(),
"[config] reset_local_data: removed active workspace marker"
);
removed_paths.push(active_workspace_marker.display().to_string());
}
for target_dir in [current_openhuman_dir, default_openhuman_dir] {
if !target_dir.exists() {
tracing::debug!(
dir = %target_dir.display(),
"[config] reset_local_data: directory already absent"
);
continue;
}
tokio::fs::remove_dir_all(target_dir)
.await
.map_err(|e| format!("Failed to remove {}: {e}", target_dir.display()))?;
tracing::debug!(
dir = %target_dir.display(),
"[config] reset_local_data: removed directory"
);
removed_paths.push(target_dir.display().to_string());
}
Ok(RpcOutcome::new(
json!({
"removed_paths": removed_paths,
"current_openhuman_dir": current_openhuman_dir.display().to_string(),
"default_openhuman_dir": default_openhuman_dir.display().to_string(),
}),
vec![
format!(
"reset local data for active config dir {}",
current_openhuman_dir.display()
),
format!(
"removed default data dir {} if present",
default_openhuman_dir.display()
),
],
))
}
pub fn snapshot_config_json(config: &Config) -> Result<serde_json::Value, String> {
let value = serde_json::to_value(config).map_err(|e| e.to_string())?;
Ok(json!({
@@ -459,3 +539,46 @@ pub fn agent_server_status() -> RpcOutcome<serde_json::Value> {
});
RpcOutcome::single_log(payload, "agent server status checked")
}
pub async fn reset_local_data() -> Result<RpcOutcome<serde_json::Value>, String> {
let config = load_config_with_timeout().await?;
let current_openhuman_dir = config_openhuman_dir(&config);
let default_openhuman_dir = default_openhuman_dir();
reset_local_data_for_paths(&current_openhuman_dir, &default_openhuman_dir).await
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn reset_local_data_removes_current_dir_default_dir_and_marker() {
let temp = tempdir().unwrap();
let default_openhuman_dir = temp.path().join("default-openhuman");
let current_openhuman_dir = temp.path().join("custom-openhuman");
let marker = active_workspace_marker_path(&default_openhuman_dir);
tokio::fs::create_dir_all(default_openhuman_dir.join("workspace"))
.await
.unwrap();
tokio::fs::create_dir_all(current_openhuman_dir.join("workspace"))
.await
.unwrap();
tokio::fs::write(&marker, "config_dir = '/tmp/custom-openhuman'\n")
.await
.unwrap();
let outcome = reset_local_data_for_paths(&current_openhuman_dir, &default_openhuman_dir)
.await
.unwrap();
assert!(!current_openhuman_dir.exists());
assert!(!default_openhuman_dir.exists());
assert!(outcome
.value
.get("removed_paths")
.and_then(|value| value.as_array())
.is_some_and(|paths| !paths.is_empty()));
}
}
+17
View File
@@ -91,6 +91,7 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
schemas("update_analytics_settings"),
schemas("get_analytics_settings"),
schemas("agent_server_status"),
schemas("reset_local_data"),
schemas("get_onboarding_completed"),
schemas("set_onboarding_completed"),
]
@@ -154,6 +155,10 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
schema: schemas("agent_server_status"),
handler: handle_agent_server_status,
},
RegisteredController {
schema: schemas("reset_local_data"),
handler: handle_reset_local_data,
},
RegisteredController {
schema: schemas("get_onboarding_completed"),
handler: handle_get_onboarding_completed,
@@ -383,6 +388,14 @@ pub fn schemas(function: &str) -> ControllerSchema {
inputs: vec![],
outputs: vec![json_output("status", "Agent server status payload.")],
},
"reset_local_data" => ControllerSchema {
namespace: "config",
function: "reset_local_data",
description:
"Delete local OpenHuman data for the active config/workspace so the next restart boots clean.",
inputs: vec![],
outputs: vec![json_output("result", "Reset result with removed paths.")],
},
"get_onboarding_completed" => ControllerSchema {
namespace: "config",
function: "get_onboarding_completed",
@@ -565,6 +578,10 @@ fn handle_agent_server_status(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async { to_json(config_rpc::agent_server_status()) })
}
fn handle_reset_local_data(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async { to_json(config_rpc::reset_local_data().await?) })
}
fn handle_get_onboarding_completed(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async { to_json(config_rpc::get_onboarding_completed().await?) })
}
+1 -3
View File
@@ -277,8 +277,6 @@ impl AccessibilityEngine {
});
}
self.request_permission(PermissionKind::ScreenRecording)
.await?;
self.request_permission(PermissionKind::Accessibility)
.await?;
self.request_permission(PermissionKind::InputMonitoring)
@@ -286,7 +284,7 @@ impl AccessibilityEngine {
let mut state = self.inner.lock().await;
state.permissions = detect_permissions();
state.last_event = Some("permissions_requested".to_string());
state.last_event = Some("permissions_requested:accessibility,input_monitoring".to_string());
Ok(state.permissions.clone())
}
+1 -1
View File
@@ -90,7 +90,7 @@ pub async fn accessibility_request_permissions() -> Result<RpcOutcome<Permission
.await?;
Ok(RpcOutcome::single_log(
permissions,
"accessibility permissions requested",
"accessibility automation permissions requested",
))
}
+3 -2
View File
@@ -96,14 +96,15 @@ pub fn schemas(function: &str) -> ControllerSchema {
"request_permissions" => ControllerSchema {
namespace: "screen_intelligence",
function: "request_permissions",
description: "Request required accessibility permissions.",
description:
"Request accessibility automation permissions without prompting for screen recording.",
inputs: vec![],
outputs: vec![json_output("permissions", "Permission status payload.")],
},
"request_permission" => ControllerSchema {
namespace: "screen_intelligence",
function: "request_permission",
description: "Request one accessibility permission.",
description: "Request one permission such as accessibility, input monitoring, or screen recording.",
inputs: vec![FieldSchema {
name: "permission",
ty: TypeSchema::String,
+3 -3
View File
@@ -1,10 +1,10 @@
use std::path::{Path, PathBuf};
use std::path::Path;
use sha2::{Digest, Sha256};
use super::registry_cache::{
cache_path, is_cache_fresh, is_local_path, local_skills_dir, read_cache, read_local_file,
registry_url, tag_categories, write_cache,
is_cache_fresh, is_local_path, local_skills_dir, read_cache, read_local_file, registry_url,
tag_categories, write_cache,
};
use super::registry_types::{
AvailableSkillEntry, InstalledSkillInfo, RegistrySkillEntry, RemoteSkillRegistry,
+2 -2
View File
@@ -9,9 +9,9 @@ use super::prompt::build_subconscious_prompt;
use super::situation_report::build_situation_report;
use super::types::{Decision, SubconsciousStatus, TickOutput, TickResult};
use crate::openhuman::config::Config;
use crate::openhuman::memory::{MemoryClient, MemoryClientRef};
use crate::openhuman::memory::MemoryClientRef;
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::{self, Duration};
+54 -6
View File
@@ -3,9 +3,9 @@
//! Exercises the full skill lifecycle through the RuntimeEngine:
//! discover → start → list tools → call tool → setup flow → tick/sync → stop
//!
//! By default uses the bundled `example-skill` from the openhuman-skills repo.
//! By default uses a preferred skill from the checked-out openhuman-skills repo.
//! Override with env vars:
//! SKILL_DEBUG_ID — skill ID to test (default: "example-skill")
//! SKILL_DEBUG_ID — skill ID to test
//! SKILL_DEBUG_DIR — path to skills directory containing skill folders
//! SKILL_DEBUG_TOOL — specific tool name to call (default: first tool found)
//! SKILL_DEBUG_TOOL_ARGS — JSON args for the tool call (default: "{}")
@@ -37,6 +37,46 @@ fn is_verbose() -> bool {
.unwrap_or(false)
}
fn manifests_in_dir(skills_dir: &Path) -> Vec<String> {
let mut ids = Vec::new();
let Ok(entries) = std::fs::read_dir(skills_dir) else {
return ids;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() || !path.join("manifest.json").exists() {
continue;
}
if let Some(name) = path.file_name().and_then(|name| name.to_str()) {
ids.push(name.to_string());
}
}
ids.sort();
ids
}
fn select_skill_id(skills_dir: &Path, env_key: &str, preferred: &[&str]) -> String {
if let Ok(skill_id) = std::env::var(env_key) {
if !skill_id.trim().is_empty() {
return skill_id;
}
}
let available = manifests_in_dir(skills_dir);
for candidate in preferred {
if available.iter().any(|id| id == candidate) {
return (*candidate).to_string();
}
}
available
.into_iter()
.next()
.unwrap_or_else(|| "example-skill".to_string())
}
fn banner(label: &str) {
let sep = "=".repeat(60);
eprintln!("\n{sep}");
@@ -104,7 +144,7 @@ fn try_find_skills_dir() -> Option<PathBuf> {
if let Some(parent) = cwd.parent() {
for entry in std::fs::read_dir(parent).into_iter().flatten().flatten() {
let candidate = entry.path().join("skills/skills");
if candidate.exists() && candidate.join("example-skill/manifest.json").exists() {
if candidate.exists() && candidate.read_dir().is_ok() {
return Some(candidate.canonicalize().unwrap());
}
}
@@ -155,8 +195,12 @@ async fn skill_full_lifecycle() {
.is_test(true)
.try_init();
let skill_id = env_or("SKILL_DEBUG_ID", "example-skill");
let skills_dir = require_skills_dir!();
let skill_id = select_skill_id(
&skills_dir,
"SKILL_DEBUG_ID",
&["server-ping", "gmail", "notion"],
);
let tmp = tempdir().expect("tempdir");
let data_dir = tmp.path().join("skills_data");
std::fs::create_dir_all(&data_dir).expect("create data_dir");
@@ -570,8 +614,12 @@ async fn skill_rapid_start_stop() {
.is_test(true)
.try_init();
let skill_id = env_or("SKILL_DEBUG_ID", "example-skill");
let skills_dir = require_skills_dir!();
let skill_id = select_skill_id(
&skills_dir,
"SKILL_DEBUG_ID",
&["server-ping", "gmail", "notion"],
);
let tmp = tempdir().expect("tempdir");
let data_dir = tmp.path().join("skills_data");
std::fs::create_dir_all(&data_dir).unwrap();
@@ -611,8 +659,8 @@ async fn skill_disconnect_flow() {
.is_test(true)
.try_init();
let skill_id = env_or("SKILL_DEBUG_ID", "example-skill");
let skills_dir = require_skills_dir!();
let skill_id = select_skill_id(&skills_dir, "SKILL_DEBUG_ID", &["gmail", "notion"]);
let tmp = tempdir().expect("tempdir");
let data_dir = tmp.path().join("skills_data");
std::fs::create_dir_all(&data_dir).unwrap();