mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
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:
co-authored by
Jwalin Shah
parent
3bb714bf96
commit
bc9e223306
@@ -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…</p>
|
||||
)}
|
||||
{loadState === 'error' && (
|
||||
<p
|
||||
className="p-4 text-xs text-stone-500"
|
||||
data-testid="privacy-load-error">
|
||||
Couldn’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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -17,3 +17,4 @@ export * from './service';
|
||||
export * from './accessibility';
|
||||
export * from './autocomplete';
|
||||
export * from './voice';
|
||||
export * from './aboutApp';
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user