feat(privacy): backend-backed capability privacy metadata + PrivacyPanel (#760)

* feat(about_app): add capability privacy metadata

Adds optional `CapabilityPrivacy { leaves_device, data_kind, destinations }`
to the about_app capability catalog so the in-app Privacy surface can be
backend-backed instead of hand-maintained. Twelve representative capabilities
are annotated for the first audited set (raw/local, derived/backend,
credentials, diagnostics, model download); remaining entries default to
None and are simply not surfaced. Wire format stays backward compatible
via skip_serializing_if.

* feat(settings): drive privacy panel from about_app capabilities

Replaces the hand-maintained privacy rows with data fetched from
openhuman.about_app_list. Only capabilities that ship privacy metadata
are rendered; loading and RPC failure both degrade gracefully and the
analytics toggle plus explanatory copy remain intact. Adds a small typed
client (utils/tauriCommands/aboutApp.ts) and focused vitest coverage for
render, omission of unannotated entries, and RPC failure.

---------

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
This commit is contained in:
Jwalin Shah
2026-04-22 14:27:47 -07:00
committed by GitHub
co-authored by Jwalin Shah
parent 3bb714bf96
commit bc9e223306
7 changed files with 450 additions and 2 deletions
@@ -1,12 +1,69 @@
import debug from 'debug';
import { useEffect, useState } from 'react';
import { useCoreState } from '../../../providers/CoreStateProvider';
import {
type Capability,
type CapabilityPrivacy,
type PrivacyDataKind,
listCapabilities,
} from '../../../utils/tauriCommands/aboutApp';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
const log = debug('privacy-panel');
interface AnnotatedCapability extends Capability {
privacy: CapabilityPrivacy;
}
const KIND_LABEL: Record<PrivacyDataKind, string> = {
raw: 'Raw user content',
derived: 'Derived signals',
credentials: 'Credentials',
diagnostics: 'Diagnostics',
metadata: 'Metadata',
};
const KIND_BADGE_CLASS: Record<PrivacyDataKind, string> = {
raw: 'bg-sage-50 text-sage-700 border-sage-200',
derived: 'bg-amber-50 text-amber-700 border-amber-200',
credentials: 'bg-stone-100 text-stone-700 border-stone-200',
diagnostics: 'bg-primary-50 text-primary-700 border-primary-200',
metadata: 'bg-stone-50 text-stone-600 border-stone-200',
};
const PrivacyPanel = () => {
const { navigateBack, breadcrumbs } = useSettingsNavigation();
const { snapshot, setAnalyticsEnabled } = useCoreState();
const analyticsEnabled = snapshot.analyticsEnabled;
const [capabilities, setCapabilities] = useState<AnnotatedCapability[]>([]);
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading');
useEffect(() => {
let cancelled = false;
log('[privacy] fetching capability catalog');
listCapabilities()
.then(items => {
if (cancelled) return;
const annotated = items.filter(
(c): c is AnnotatedCapability => c.privacy !== undefined && c.privacy !== null
);
log('[privacy] catalog ready', { total: items.length, annotated: annotated.length });
setCapabilities(annotated);
setLoadState('ready');
})
.catch(err => {
if (cancelled) return;
console.warn('[privacy] failed to load capability catalog:', err);
setLoadState('error');
});
return () => {
cancelled = true;
};
}, []);
const handleToggleAnalytics = async () => {
const newValue = !analyticsEnabled;
try {
@@ -27,6 +84,65 @@ const PrivacyPanel = () => {
<div>
<div className="p-4 space-y-4">
{/* What leaves my computer */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-3 px-1">
What leaves your computer
</h3>
<div className="bg-white rounded-xl border border-stone-200 overflow-hidden">
{loadState === 'loading' && (
<p className="p-4 text-xs text-stone-500">Loading privacy details&hellip;</p>
)}
{loadState === 'error' && (
<p
className="p-4 text-xs text-stone-500"
data-testid="privacy-load-error">
Couldn&rsquo;t load the live privacy list. Analytics controls below still work.
</p>
)}
{loadState === 'ready' && capabilities.length === 0 && (
<p className="p-4 text-xs text-stone-500">
No capabilities currently disclose data movement.
</p>
)}
{loadState === 'ready' && capabilities.length > 0 && (
<ul
className="divide-y divide-stone-100"
data-testid="privacy-capability-list">
{capabilities.map(cap => (
<li
key={cap.id}
className="p-4"
data-testid={`privacy-row-${cap.id}`}>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-stone-900">{cap.name}</p>
<p className="text-xs text-stone-500 mt-1 leading-relaxed">
{cap.description}
</p>
{cap.privacy.destinations.length > 0 && (
<p className="text-xs text-stone-400 mt-1">
Sent to: {cap.privacy.destinations.join(', ')}
</p>
)}
</div>
<div className="flex flex-col items-end gap-1 shrink-0">
<span
className={`text-[10px] uppercase tracking-wider px-2 py-0.5 rounded border ${KIND_BADGE_CLASS[cap.privacy.data_kind]}`}>
{KIND_LABEL[cap.privacy.data_kind]}
</span>
<span className="text-[10px] text-stone-500">
{cap.privacy.leaves_device ? 'Leaves device' : 'Stays local'}
</span>
</div>
</div>
</li>
))}
</ul>
)}
</div>
</div>
{/* Analytics Section */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-3 px-1">
@@ -0,0 +1,99 @@
import { screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../../../../test/test-utils';
import { type Capability, listCapabilities } from '../../../../utils/tauriCommands/aboutApp';
import PrivacyPanel from '../PrivacyPanel';
vi.mock('../../../../utils/tauriCommands/aboutApp', () => ({
listCapabilities: vi.fn(),
}));
vi.mock('../../../../providers/CoreStateProvider', () => ({
useCoreState: () => ({
snapshot: { analyticsEnabled: false },
setAnalyticsEnabled: vi.fn(),
}),
}));
vi.mock('../../hooks/useSettingsNavigation', () => ({
useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }),
}));
const annotated: Capability = {
id: 'conversation.send_text',
name: 'Send Text Messages',
domain: 'conversation',
category: 'conversation',
description: 'Send typed messages to the assistant in a conversation.',
how_to: 'Conversations > Message composer',
status: 'stable',
privacy: {
leaves_device: true,
data_kind: 'derived',
destinations: ['OpenHuman backend', 'TinyHumans Neocortex'],
},
};
const localOnly: Capability = {
id: 'local_ai.embed_text',
name: 'Embed Text',
domain: 'local_ai',
category: 'local_ai',
description: 'Generate embeddings locally.',
how_to: 'Local AI',
status: 'stable',
privacy: { leaves_device: false, data_kind: 'raw', destinations: [] },
};
const unannotated: Capability = {
id: 'conversation.create',
name: 'Create Conversations',
domain: 'conversation',
category: 'conversation',
description: 'Start a new conversation thread.',
how_to: 'Conversations',
status: 'stable',
};
describe('PrivacyPanel', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders annotated capabilities returned by about_app.list', async () => {
vi.mocked(listCapabilities).mockResolvedValue([annotated, localOnly]);
renderWithProviders(<PrivacyPanel />);
await waitFor(() => {
expect(screen.getByTestId('privacy-capability-list')).toBeTruthy();
});
expect(screen.getByTestId('privacy-row-conversation.send_text')).toBeTruthy();
expect(screen.getByTestId('privacy-row-local_ai.embed_text')).toBeTruthy();
expect(screen.getByText(/OpenHuman backend, TinyHumans Neocortex/)).toBeTruthy();
expect(screen.getByText('Stays local')).toBeTruthy();
});
it('omits capabilities without privacy metadata', async () => {
vi.mocked(listCapabilities).mockResolvedValue([annotated, unannotated]);
renderWithProviders(<PrivacyPanel />);
await waitFor(() => {
expect(screen.getByTestId('privacy-row-conversation.send_text')).toBeTruthy();
});
expect(screen.queryByTestId('privacy-row-conversation.create')).toBeNull();
});
it('shows graceful fallback when the RPC fails and keeps analytics toggle visible', async () => {
vi.mocked(listCapabilities).mockRejectedValue(new Error('boom'));
renderWithProviders(<PrivacyPanel />);
await waitFor(() => {
expect(screen.getByTestId('privacy-load-error')).toBeTruthy();
});
expect(screen.queryByTestId('privacy-capability-list')).toBeNull();
// Analytics toggle still rendered
expect(screen.getByRole('switch')).toBeTruthy();
});
});
+61
View File
@@ -0,0 +1,61 @@
/**
* About-app capability catalog client.
*
* Thin wrapper around the `openhuman.about_app_*` JSON-RPC methods exposed by
* the Rust core (`src/openhuman/about_app/schemas.rs`). The Privacy surface is
* the first consumer; future panels can reuse the same types.
*/
import { callCoreRpc } from '../../services/coreRpcClient';
import { CommandResponse } from './common';
export type CapabilityCategory =
| 'conversation'
| 'intelligence'
| 'skills'
| 'local_ai'
| 'team'
| 'settings'
| 'auth'
| 'screen_intelligence'
| 'channels'
| 'automation';
export type CapabilityStatus = 'stable' | 'beta' | 'coming_soon' | 'deprecated';
export type PrivacyDataKind =
| 'raw'
| 'derived'
| 'credentials'
| 'diagnostics'
| 'metadata';
export interface CapabilityPrivacy {
leaves_device: boolean;
data_kind: PrivacyDataKind;
destinations: string[];
}
export interface Capability {
id: string;
name: string;
domain: string;
category: CapabilityCategory;
description: string;
how_to: string;
status: CapabilityStatus;
privacy?: CapabilityPrivacy;
}
export async function listCapabilities(category?: CapabilityCategory): Promise<Capability[]> {
const response = await callCoreRpc<CommandResponse<Capability[]> | Capability[]>({
method: 'openhuman.about_app_list',
params: category ? { category } : {},
});
// RpcOutcome::single_log emits {result, logs}; bare arrays are handled too
// for forward-compat if logs ever go away.
if (Array.isArray(response)) {
return response;
}
return response.result;
}
+1
View File
@@ -17,3 +17,4 @@ export * from './service';
export * from './accessibility';
export * from './autocomplete';
export * from './voice';
export * from './aboutApp';
+137 -1
View File
@@ -1,7 +1,39 @@
use std::collections::BTreeSet;
use std::sync::OnceLock;
use super::types::{Capability, CapabilityCategory, CapabilityStatus};
use super::types::{
Capability, CapabilityCategory, CapabilityPrivacy, CapabilityStatus, PrivacyDataKind,
};
const LOCAL_RAW: Option<CapabilityPrivacy> = Some(CapabilityPrivacy {
leaves_device: false,
data_kind: PrivacyDataKind::Raw,
destinations: &[],
});
const DERIVED_TO_BACKEND: Option<CapabilityPrivacy> = Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::Derived,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
});
const LOCAL_CREDENTIALS: Option<CapabilityPrivacy> = Some(CapabilityPrivacy {
leaves_device: false,
data_kind: PrivacyDataKind::Credentials,
destinations: &[],
});
const DIAGNOSTICS_TO_BACKEND: Option<CapabilityPrivacy> = Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::Diagnostics,
destinations: &["OpenHuman backend"],
});
const MODEL_DOWNLOAD: Option<CapabilityPrivacy> = Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::Metadata,
destinations: &["Hugging Face"],
});
const CAPABILITIES: &[Capability] = &[
Capability {
@@ -12,6 +44,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Start a new conversation thread with the assistant.",
how_to: "Conversations",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "conversation.send_text",
@@ -21,6 +54,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Send typed messages to the assistant in a conversation.",
how_to: "Conversations > Message composer",
status: CapabilityStatus::Stable,
privacy: DERIVED_TO_BACKEND,
},
Capability {
id: "conversation.send_voice",
@@ -30,6 +64,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Record or attach voice input and send it as a message.",
how_to: "Conversations > Voice input",
status: CapabilityStatus::Beta,
privacy: DERIVED_TO_BACKEND,
},
Capability {
id: "conversation.inline_autocomplete",
@@ -39,6 +74,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Show predictive inline text suggestions while you type.",
how_to: "Settings > Inline Autocomplete",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "conversation.copy_messages",
@@ -48,6 +84,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Copy individual assistant or user messages for reuse elsewhere.",
how_to: "Conversations > Message actions",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "conversation.delete_conversations",
@@ -57,6 +94,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Remove existing conversation threads from the app.",
how_to: "Conversations > Thread actions",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "conversation.suggested_questions",
@@ -66,6 +104,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Offer prompt suggestions to help continue a conversation.",
how_to: "Home or Conversations > Suggested prompts",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "conversation.tool_execution_timeline",
@@ -75,6 +114,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Show the sequence of tool calls and actions used to answer a request.",
how_to: "Conversations > Tool timeline",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "intelligence.analyze_actionable_items",
@@ -84,6 +124,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Extract and summarize actionable items from your activity and conversations.",
how_to: "Intelligence",
status: CapabilityStatus::Stable,
privacy: DERIVED_TO_BACKEND,
},
Capability {
id: "intelligence.filter_actionable_items",
@@ -93,6 +134,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Search and filter actionable items to focus on what matters now.",
how_to: "Intelligence > Filters and search",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "intelligence.mark_actionable_item_complete",
@@ -102,6 +144,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Mark an actionable item as completed.",
how_to: "Intelligence > Item actions",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "intelligence.dismiss_actionable_item",
@@ -111,6 +154,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Dismiss irrelevant or already handled actionable items.",
how_to: "Intelligence > Item actions",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "intelligence.snooze_actionable_item",
@@ -120,6 +164,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Temporarily hide an actionable item until later.",
how_to: "Intelligence > Item actions",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "intelligence.undo_action",
@@ -129,6 +174,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Undo a recent complete, dismiss, or snooze action.",
how_to: "Intelligence > Undo snackbar or item history",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "intelligence.memory_workspace",
@@ -138,6 +184,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Inspect or debug the app's memory workspace and stored knowledge.",
how_to: "Settings > Memory Debug",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "skills.discover",
@@ -147,6 +194,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Browse available skills that can extend the app.",
how_to: "Skills",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "skills.install",
@@ -156,6 +204,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Install a skill into the local workspace.",
how_to: "Skills > Install",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "skills.configure",
@@ -165,6 +214,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Open skill setup and update skill-specific configuration.",
how_to: "Skills > Setup or Settings > Connections",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "skills.connection_status",
@@ -174,6 +224,7 @@ const CAPABILITIES: &[Capability] = &[
description: "See whether a skill-backed integration is connected, offline, or needs setup.",
how_to: "Skills or Settings > Connections",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "skills.sync_manual",
@@ -183,6 +234,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Trigger a manual data sync for a skill integration.",
how_to: "Skills > Skill card > Sync",
status: CapabilityStatus::Beta,
privacy: DERIVED_TO_BACKEND,
},
Capability {
id: "skills.run_apify_actors",
@@ -192,6 +244,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Launch Apify scrapers and automation actors, then inspect run status and collected results.",
how_to: "Conversations > Ask the assistant to run an Apify actor",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "skills.toggle_enabled",
@@ -201,6 +254,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Turn individual skills on or off without uninstalling them.",
how_to: "Settings > Developer Options > Skills",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "skills.open_connections_hub",
@@ -210,6 +264,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Browse the dedicated connections hub for external skill-backed integrations.",
how_to: "Settings > Connections",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "skills.connect_google",
@@ -219,6 +274,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Connect Google services for email, contacts, and calendar workflows.",
how_to: "Settings > Connections",
status: CapabilityStatus::ComingSoon,
privacy: LOCAL_CREDENTIALS,
},
Capability {
id: "skills.connect_notion",
@@ -228,6 +284,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Connect Notion for workspace sync and productivity workflows.",
how_to: "Settings > Connections",
status: CapabilityStatus::ComingSoon,
privacy: LOCAL_CREDENTIALS,
},
Capability {
id: "skills.connect_web3_wallet",
@@ -237,6 +294,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Connect a wallet for crypto workflows and onchain actions.",
how_to: "Settings > Connections",
status: CapabilityStatus::ComingSoon,
privacy: None,
},
Capability {
id: "skills.connect_crypto_exchange",
@@ -246,6 +304,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Connect supported exchanges for trading and portfolio workflows.",
how_to: "Settings > Connections",
status: CapabilityStatus::ComingSoon,
privacy: None,
},
Capability {
id: "local_ai.download_model",
@@ -255,6 +314,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Download and bootstrap local AI runtimes and model bundles.",
how_to: "Settings > Local AI Model",
status: CapabilityStatus::Beta,
privacy: MODEL_DOWNLOAD,
},
Capability {
id: "local_ai.manage_model_assets",
@@ -264,6 +324,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Inspect asset status and download specific chat, vision, embedding, STT, or TTS assets.",
how_to: "Settings > Local AI Model > Advanced > Capability Assets",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "local_ai.embed_text",
@@ -273,6 +334,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Create local vector embeddings for text input.",
how_to: "Settings > Local AI Model > Advanced > Test Embeddings",
status: CapabilityStatus::Beta,
privacy: LOCAL_RAW,
},
Capability {
id: "local_ai.speech_to_text",
@@ -282,6 +344,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Transcribe audio into text using local speech recognition.",
how_to: "Settings > Local AI Model > Advanced > Test Voice Input",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "local_ai.text_to_speech",
@@ -291,6 +354,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Synthesize speech from text using local voice models.",
how_to: "Settings > Local AI Model > Advanced > Test Voice Output",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "local_ai.vision_processing",
@@ -300,6 +364,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Run vision prompts against images using a local multimodal model.",
how_to: "Settings > Local AI Model > Advanced > Test Vision Prompt",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "local_ai.direct_prompting",
@@ -309,6 +374,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Send a direct prompt to the local model without using the cloud API.",
how_to: "Settings > Local AI Model > Advanced > Test Custom Prompt",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "team.create",
@@ -318,6 +384,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Create a team and start collaborating with shared billing and members.",
how_to: "Settings > Team",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "team.join_via_invite_code",
@@ -327,6 +394,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Join an existing team using an invite code.",
how_to: "Invites > Redeem an Invite Code",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "team.switch_active_team",
@@ -336,6 +404,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Switch which team is currently active in the app.",
how_to: "Settings > Team",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "team.leave",
@@ -345,6 +414,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Leave a team that you no longer want to participate in.",
how_to: "Settings > Team",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "team.manage_members",
@@ -354,6 +424,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Review members and change team roles when you have permission.",
how_to: "Settings > Team > Manage team > Members",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "team.generate_invite_codes",
@@ -363,6 +434,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Create invite codes to bring new members into a team.",
how_to: "Settings > Team > Manage team > Invites",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "team.track_invite_usage",
@@ -372,6 +444,7 @@ const CAPABILITIES: &[Capability] = &[
description: "View invite usage counts, limits, and revoke team invites.",
how_to: "Settings > Team > Manage team > Invites",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "auth.login_oauth",
@@ -381,6 +454,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Sign in with the app's supported provider-based authentication flow.",
how_to: "Welcome",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "auth.onboarding_setup",
@@ -390,6 +464,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Walk through onboarding to configure initial permissions and preferences.",
how_to: "Onboarding",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "auth.configure_tool_access",
@@ -399,6 +474,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Choose which built-in tools OpenHuman can use on your behalf during setup.",
how_to: "Onboarding > Enable Tools",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "auth.backup_recovery_phrase",
@@ -408,6 +484,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Generate and save a recovery phrase used to secure and restore encrypted app data.",
how_to: "Onboarding > Recovery Phrase",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "auth.import_recovery_phrase",
@@ -417,6 +494,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Import an existing recovery phrase to restore encrypted app data.",
how_to: "Onboarding > Recovery Phrase > I already have a recovery phrase",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "auth.logout",
@@ -426,6 +504,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Sign out of the current session.",
how_to: "Settings > Log out",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "screen_intelligence.toggle_monitoring",
@@ -435,6 +514,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Turn desktop screen intelligence capture on or off.",
how_to: "Settings > Screen Intelligence",
status: CapabilityStatus::Beta,
privacy: LOCAL_RAW,
},
Capability {
id: "screen_intelligence.manage_accessibility_permissions",
@@ -444,6 +524,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Review and grant the accessibility permissions required for desktop assistance.",
how_to: "Onboarding > Screen permissions or Settings > Accessibility Automation",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "screen_intelligence.review_vision_data",
@@ -453,6 +534,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Inspect the captured screen intelligence and related vision summaries.",
how_to: "Settings > Screen Intelligence",
status: CapabilityStatus::Beta,
privacy: LOCAL_RAW,
},
Capability {
id: "screen_intelligence.configure_capture_fps",
@@ -462,6 +544,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Tune the screen capture frame rate used by screen intelligence.",
how_to: "Settings > Screen Intelligence",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "screen_intelligence.app_whitelist",
@@ -471,6 +554,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Allow screen intelligence only for selected applications.",
how_to: "Settings > Screen Intelligence",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "screen_intelligence.app_blacklist",
@@ -480,6 +564,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Exclude selected applications from screen intelligence capture.",
how_to: "Settings > Screen Intelligence",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "channels.connect_platform",
@@ -489,6 +574,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Connect supported messaging platforms such as Telegram, Discord, or Slack.",
how_to: "Settings > Messaging Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "channels.disconnect_platform",
@@ -498,6 +584,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Disconnect a previously configured messaging platform.",
how_to: "Settings > Messaging Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "channels.test_credentials",
@@ -507,6 +594,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Validate platform credentials or connection state before using a channel.",
how_to: "Settings > Messaging Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "channels.set_default_channel",
@@ -516,6 +604,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Choose which messaging channel should be used by default.",
how_to: "Settings > Messaging Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "settings.configure_ai",
@@ -525,6 +614,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Adjust AI-related settings and agent behavior preferences.",
how_to: "Settings > Developer Options > AI Configuration",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "settings.manage_privacy_analytics",
@@ -534,6 +624,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Control privacy, analytics, and related data handling preferences.",
how_to: "Settings > Privacy (direct route)",
status: CapabilityStatus::Stable,
privacy: DIAGNOSTICS_TO_BACKEND,
},
Capability {
id: "settings.view_billing",
@@ -543,6 +634,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Open subscription, included usage, and pay-as-you-go billing views for your active team.",
how_to: "Settings > Billing & Usage",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "settings.manage_subscription_plan",
@@ -552,6 +644,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Upgrade plans or open the billing portal to manage subscription-backed usage tiers.",
how_to: "Settings > Billing & Usage",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "settings.manage_credits",
@@ -561,6 +654,7 @@ const CAPABILITIES: &[Capability] = &[
description: "View pay-as-you-go credit balances, top up overage credits, and configure auto-recharge.",
how_to: "Settings > Billing & Usage",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "settings.add_payment_methods",
@@ -570,6 +664,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Add or manage saved payment methods for billing and auto-recharge.",
how_to: "Settings > Billing & Usage > Payment Methods",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "settings.developer_options",
@@ -579,6 +674,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Open developer-focused panels for diagnostics, skills, AI config, and memory tools.",
how_to: "Settings > Developer Options",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "settings.debug_webhooks",
@@ -589,6 +685,7 @@ const CAPABILITIES: &[Capability] = &[
"Inspect Composio trigger history and find the daily JSONL archive files stored by the app.",
how_to: "Settings > Developer Options > Webhooks",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "settings.manage_service",
@@ -598,6 +695,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Install, start, stop, restart, uninstall, or inspect the optional desktop background service.",
how_to: "Settings > Developer Options > Tauri Commands",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "settings.clear_app_data",
@@ -607,6 +705,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Sign out and permanently clear local app data, including skills data.",
how_to: "Settings > Log Out & Clear App Data",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "settings.delete_all_data",
@@ -616,6 +715,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Delete all local data and reset the app from the destructive settings section.",
how_to: "Settings > Delete All Data",
status: CapabilityStatus::ComingSoon,
privacy: None,
},
Capability {
id: "automation.view_cron_jobs",
@@ -625,6 +725,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Review scheduled jobs available to the runtime.",
how_to: "Settings > Cron Jobs",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "automation.set_job_intervals",
@@ -634,6 +735,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Configure how often a scheduled job should run.",
how_to: "Settings > Cron Jobs",
status: CapabilityStatus::Stable,
privacy: None,
},
Capability {
id: "automation.view_execution_history",
@@ -643,6 +745,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Inspect past runs and results for scheduled jobs.",
how_to: "Settings > Cron Jobs",
status: CapabilityStatus::Beta,
privacy: None,
},
// ── Proactive agents ─────────────────────────────────────────────────────
Capability {
@@ -653,6 +756,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Daily proactive agent that reviews calendar, tasks, emails, and market context to deliver a morning summary.",
how_to: "Automatic after onboarding (runs daily at 7 AM). Adjust schedule via Settings > Cron Jobs.",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "automation.welcome_agent",
@@ -662,6 +766,7 @@ const CAPABILITIES: &[Capability] = &[
description: "One-shot agent that delivers a personalized, witty welcome message after onboarding completes.",
how_to: "Automatic — triggered once after onboarding.",
status: CapabilityStatus::Beta,
privacy: None,
},
// ── Update ──────────────────────────────────────────────────────────────
Capability {
@@ -672,6 +777,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Query GitHub Releases to see if a newer core binary is available.",
how_to: "Settings > Developer Options > Check for Updates",
status: CapabilityStatus::Beta,
privacy: DIAGNOSTICS_TO_BACKEND,
},
Capability {
id: "update.apply",
@@ -681,6 +787,7 @@ const CAPABILITIES: &[Capability] = &[
description: "Download and stage a newer core binary, then restart the sidecar.",
how_to: "Settings > Developer Options > Apply Update",
status: CapabilityStatus::Beta,
privacy: None,
},
];
@@ -798,6 +905,35 @@ mod tests {
assert!(!capabilities.is_empty());
}
#[test]
fn annotated_capability_exposes_privacy_metadata() {
let cap = lookup("conversation.send_text").expect("capability exists");
let privacy = cap.privacy.expect("conversation.send_text annotated");
assert!(privacy.leaves_device);
assert_eq!(privacy.data_kind, PrivacyDataKind::Derived);
assert!(privacy.destinations.contains(&"OpenHuman backend"));
}
#[test]
fn local_only_capability_marks_no_destinations() {
let cap = lookup("local_ai.embed_text").expect("capability exists");
let privacy = cap.privacy.expect("local_ai.embed_text annotated");
assert!(!privacy.leaves_device);
assert_eq!(privacy.data_kind, PrivacyDataKind::Raw);
assert!(privacy.destinations.is_empty());
}
#[test]
fn unannotated_capability_serializes_without_privacy_field() {
let cap = lookup("conversation.create").expect("capability exists");
assert!(cap.privacy.is_none());
let json = serde_json::to_value(cap).expect("serialize capability");
assert!(
json.get("privacy").is_none(),
"privacy field must be omitted when None: {json}"
);
}
#[test]
fn catalog_includes_additional_user_facing_surfaces() {
let ids: BTreeSet<&str> = all_capabilities()
+3 -1
View File
@@ -14,4 +14,6 @@ pub use ops::{list_capabilities, lookup_capability, search_capabilities};
pub use schemas::{
about_app_schemas, all_about_app_controller_schemas, all_about_app_registered_controllers,
};
pub use types::{Capability, CapabilityCategory, CapabilityStatus};
pub use types::{
Capability, CapabilityCategory, CapabilityPrivacy, CapabilityStatus, PrivacyDataKind,
};
+33
View File
@@ -118,6 +118,39 @@ pub struct Capability {
pub description: &'static str,
pub how_to: &'static str,
pub status: CapabilityStatus,
/// Optional privacy disclosure metadata. `None` means the capability has not
/// been annotated yet — UI should treat absence as "unknown", not "safe".
#[serde(skip_serializing_if = "Option::is_none")]
pub privacy: Option<CapabilityPrivacy>,
}
/// Per-capability privacy disclosure consumed by the in-app Privacy surface.
///
/// Source of truth for "what leaves my computer" — kept narrow on purpose so
/// every field is something the UI can render directly without further mapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct CapabilityPrivacy {
/// True when invoking this capability sends *some* data off the device.
pub leaves_device: bool,
/// Classifies what kind of data leaves (or stays).
pub data_kind: PrivacyDataKind,
/// Stable, human-readable destinations data may flow to (empty when local-only).
pub destinations: &'static [&'static str],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PrivacyDataKind {
/// Raw user content (messages, screen frames, audio) — kept local.
Raw,
/// Derived signals (embeddings, summaries, prompts) — may be sent to backends.
Derived,
/// OAuth tokens, API keys, wallet connections — stored locally, never logged.
Credentials,
/// Anonymous analytics, crash reports, version pings.
Diagnostics,
/// Non-sensitive metadata (capability ids, feature flags, settings shape).
Metadata,
}
#[cfg(test)]