fix(webview/meet): gate orchestrator handoff on user opt-in (#1299)#1310
Conversation
…tinyhumansai#1299) New top-level config section `meet` with a single field `auto_orchestrator_handoff: bool` defaulting to `false`. Mirrors the shape of `observability.analytics_enabled` — same patch struct, same load-and-apply ops, same controller registration pattern. Adds the `update_meet_settings` and `get_meet_settings` JSON-RPC controllers under the `config` namespace so the React layer can read and toggle the flag without going through the full app-state snapshot. Default-OFF semantics are deliberate: until the user explicitly opts in, ending a Google Meet call must NOT hand the transcript to the orchestrator agent. See issue tinyhumansai#1299 for the privacy hole this gates. Tests: - 5 MeetConfig unit tests (default OFF, helper fn, deserialize default, explicit flag, round-trip). - `apply_meet_settings_updates_handoff_flag` covers ON/OFF/no-op patch. - Schema-registry contract test extended with the two new method names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…humansai#1299) Surface the new `config.meet.auto_orchestrator_handoff` flag through the `openhuman.app_state_snapshot` RPC so the React shell can read it alongside other gating flags (`onboardingCompleted`, `chatOnboardingCompleted`, `analyticsEnabled`). Extends the existing `json_rpc_app_state_snapshot_returns_runtime_shape` e2e test with a `meetAutoOrchestratorHandoff: false` default assertion. Fresh users must see OFF on first boot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er (tinyhumansai#1299) Mirrors the existing analytics-toggle plumbing: - New `openhumanGetMeetSettings` / `openhumanUpdateMeetSettings` Tauri RPC bindings in `utils/tauriCommands/config.ts` (`openhuman.<get|update>_meet_settings`). - `CoreAppSnapshot.meetAutoOrchestratorHandoff: boolean` (default `false` in `emptySnapshot`) and the corresponding optional field in `AppStateSnapshotResult` for backward-compat with older core builds. - `CoreStateProvider` exposes `setMeetAutoOrchestratorHandoff` with the same optimistic-commit + refresh pattern as `setAnalyticsEnabled`. - Existing fixtures (`store.test.ts`, both `CoreStateProvider.*.tsx`, `socketSelectors.test.ts`) updated for the new required field. The actual privacy gate that consumes this state lands in the next commit. This commit is purely the plumbing layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nsai#1299) THE FIX. Previously `flushMeetingSession` unconditionally called `handoffToOrchestrator` whenever a Google Meet call ended with at least one caption segment. That handoff feeds the transcript to the orchestrator agent with a prompt explicitly inviting tool execution ("proactively handle it now"); since the orchestrator has the full Slack tool surface, it routinely posted meeting summaries to `#general" without user consent. Now wraps the call in `maybeHandoffToOrchestrator` which: 1. Reads `meet.auto_orchestrator_handoff` fresh from the core RPC on every `meet_call_ended` (rare event; ~30ms; always-current). 2. Only invokes `handoffToOrchestrator` when the flag is explicitly `true`. 3. Fails closed (no handoff) if the settings RPC throws — privacy conservatism beats fail-open. Memory ingest at line 594 is unchanged: transcripts still land in memory by default. Only the auto-action path is gated. Tests: - 4 vitest cases in `webviewAccountService.meetHandoffGate.test.ts`: toggle OFF skips, missing field skips, RPC throws skips, toggle ON fires both `threadApi.createNewThread` and `chatSend". - Module exposes a `__testInternals" namespace so the gate can be exercised without driving the full event-listener pipeline. Closes tinyhumansai#1299. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nyhumansai#1299) User-facing opt-in surface for the new `meet.auto_orchestrator_handoff` flag. Renders below the existing Anonymized Analytics section and uses the same toggle markup so the visual rhythm of the panel is consistent. Copy explains exactly what the handoff will do ("drafting messages, scheduling follow-ups, posting summaries to your connected Slack workspace") so the user understands the privacy tradeoff before flipping it on. Off by default. Test: extends `PrivacyPanel.test.tsx` with a click-through case verifying `setMeetAutoOrchestratorHandoff(true)` is called on toggle click. Existing graceful-fallback test loosened from `getByRole` to `getAllByRole(...).length >= 2` since two toggles are now rendered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nyhumansai#1299) Frontend was calling `openhuman.update_meet_settings" / `openhuman.get_meet_settings` but the controller registry exposes the canonical `openhuman.config_update_meet_settings" / `openhuman.config_get_meet_settings` (namespace + function pattern shared by every other config RPC like `openhuman.config_update_runtime_settings`). Without the prefix `schema_for_rpc_method" returns None and dispatch falls through to `unknown_method method=openhuman.update_meet_settings". Manifests as toggle stuck mid-animation — optimistic UI flip fires, RPC fails, post-RPC `refresh()` rolls the snapshot back so aria-checked never lands. The Rust side was always correct; only the TS bindings were wrong. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR adds an opt-in privacy gate for Google Meet orchestrator handoffs: a new config flag (meet.auto_orchestrator_handoff, default false) flows through backend config, RPC schemas, Tauri command wrappers, core state, a Settings toggle, and a gated handoff path that only calls the orchestrator when enabled. Tests and docs added/updated across backend and frontend. ChangesMeet Orchestrator Handoff Privacy Gate
Sequence DiagramsequenceDiagram
actor User
participant PrivacyPanel
participant CoreStateProvider
participant TauriAPI
participant Backend as Backend Config RPC
User->>PrivacyPanel: Toggle "Meeting follow-ups"
PrivacyPanel->>CoreStateProvider: setMeetAutoOrchestratorHandoff(enabled)
CoreStateProvider->>CoreStateProvider: Optimistically update snapshot
CoreStateProvider->>TauriAPI: openhumanUpdateMeetSettings({auto_orchestrator_handoff})
TauriAPI->>Backend: config_update_meet_settings
Backend->>Backend: Apply patch & persist
Backend-->>TauriAPI: Return ConfigSnapshot
TauriAPI-->>CoreStateProvider: Return success
CoreStateProvider->>CoreStateProvider: Refresh full snapshot
CoreStateProvider-->>PrivacyPanel: Notify subscribers
PrivacyPanel-->>User: Reflect new state
sequenceDiagram
participant MeetCallFlow
participant WebviewService as webviewAccountService
participant TauriAPI as Tauri API
participant Backend as Backend Config
participant Orchestrator
MeetCallFlow->>WebviewService: flushMeetingSession() on call_ended
WebviewService->>WebviewService: Persist transcript to memory
WebviewService->>WebviewService: maybeHandoffToOrchestrator(...)
maybeHandoffToOrchestrator->>TauriAPI: openhumanGetMeetSettings()
TauriAPI->>Backend: config_get_meet_settings
Backend-->>TauriAPI: {auto_orchestrator_handoff: bool}
TauriAPI-->>WebviewService: Settings retrieved
alt Feature Enabled (true)
WebviewService->>Orchestrator: handoffToOrchestrator() (create thread & send transcript)
else Feature Disabled (false)
WebviewService->>WebviewService: Log skip, return
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
app/src/providers/__tests__/CoreStateProvider.identityFlip.test.tsx (1)
77-77: ⚡ Quick winAlign the mocked refresh payload with the current snapshot shape too.
resetCoreStateStore()now includesmeetAutoOrchestratorHandoff, but themakeSnapshot()helper used byfetchCoreAppSnapshot()still omits it. That means every refresh path in this suite is exercising only the legacy-core payload and would miss a regression that drops the new flag during provider refresh.Suggested tweak
function makeSnapshot(overrides: { userId?: string | null; sessionToken?: string | null; isAuthenticated?: boolean; }): Snapshot { return { @@ onboardingCompleted: false, chatOnboardingCompleted: false, analyticsEnabled: false, + meetAutoOrchestratorHandoff: false, localState: {}, runtime: { screenIntelligence: null as never, localAi: null as never,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/providers/__tests__/CoreStateProvider.identityFlip.test.tsx` at line 77, The mocked snapshot returned by makeSnapshot() used by fetchCoreAppSnapshot() is missing the new meetAutoOrchestratorHandoff flag, so update makeSnapshot() to include meetAutoOrchestratorHandoff with the same default value used in resetCoreStateStore(); ensure any helper creating the refresh payload includes that flag so refresh paths in CoreStateProvider.identityFlip.test.tsx exercise the current snapshot shape and will catch regressions that drop the flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/components/settings/panels/PrivacyPanel.tsx`:
- Around line 198-206: The switch button rendered with role="switch"
(data-testid="privacy-meet-handoff-toggle") is missing an accessible name;
update the element to provide one (e.g., add aria-label or aria-labelledby
referencing a visible label) so assistive tech can announce it, ensuring the
label reflects the meetAutoHandoff state and that handleToggleMeetAutoHandoff
continues to be used for onClick; prefer aria-checked already present and make
sure the chosen label text matches the surrounding UI copy for Meeting
follow-ups.
In `@app/src/services/coreStateApi.ts`:
- Around line 36-41: fetchCoreAppSnapshot() currently returns the raw RPC
payload so meetAutoOrchestratorHandoff can be undefined for older cores;
normalize it at the API boundary by mapping the returned snapshot (in
fetchCoreAppSnapshot or the place that builds the returned AppSnapshot) to
ensure meetAutoOrchestratorHandoff is always a boolean (e.g., set
meetAutoOrchestratorHandoff: snapshot.meetAutoOrchestratorHandoff ?? false)
before returning, or alternatively update the comment if you choose not to
normalize.
In `@src/openhuman/config/schemas.rs`:
- Around line 837-858: Both new RPC handlers lack diagnostic logs; add
stable-prefix debug/trace logs for entry, successful exit, and error paths in
handle_update_meet_settings and handle_get_meet_settings. Specifically,
instrument the start of handle_update_meet_settings and handle_get_meet_settings
with a debug/trace log like "rpc:meet:handle_update_meet_settings:entry" /
"rpc:meet:handle_get_meet_settings:entry", log the outcome on success with a
message including relevant state (e.g., the patch or returned
auto_orchestrator_handoff) using debug, and log errors on the catch/Err path
with an error-level message that includes the same stable prefix and the error
details; ensure logs use the project's log/tracing crate and keep messages
concise and stable for observability.
---
Nitpick comments:
In `@app/src/providers/__tests__/CoreStateProvider.identityFlip.test.tsx`:
- Line 77: The mocked snapshot returned by makeSnapshot() used by
fetchCoreAppSnapshot() is missing the new meetAutoOrchestratorHandoff flag, so
update makeSnapshot() to include meetAutoOrchestratorHandoff with the same
default value used in resetCoreStateStore(); ensure any helper creating the
refresh payload includes that flag so refresh paths in
CoreStateProvider.identityFlip.test.tsx exercise the current snapshot shape and
will catch regressions that drop the flag.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bb7a829f-57c2-41e1-b85f-dfcfd21d92f5
📒 Files selected for processing (23)
app/src/components/settings/panels/PrivacyPanel.tsxapp/src/components/settings/panels/__tests__/PrivacyPanel.test.tsxapp/src/lib/coreState/__tests__/store.test.tsapp/src/lib/coreState/store.tsapp/src/providers/CoreStateProvider.tsxapp/src/providers/__tests__/CoreStateProvider.identityFlip.test.tsxapp/src/providers/__tests__/CoreStateProvider.test.tsxapp/src/services/__tests__/webviewAccountService.meetHandoffGate.test.tsapp/src/services/coreStateApi.tsapp/src/services/webviewAccountService.tsapp/src/store/__tests__/socketSelectors.test.tsapp/src/utils/tauriCommands/config.tssrc/openhuman/app_state/ops.rssrc/openhuman/config/README.mdsrc/openhuman/config/mod.rssrc/openhuman/config/ops.rssrc/openhuman/config/ops_tests.rssrc/openhuman/config/schema/meet.rssrc/openhuman/config/schema/mod.rssrc/openhuman/config/schema/types.rssrc/openhuman/config/schemas.rssrc/openhuman/config/schemas_tests.rstests/json_rpc_e2e.rs
Three review comments resolved: - `PrivacyPanel.tsx`: add `aria-label` to the new Meeting follow-ups toggle so assistive tech can announce the role=switch control (CR major / a11y). - `coreStateApi.ts`: normalise the optional `meetAutoOrchestratorHandoff` field to `false` at the API boundary so older core builds without the field never surface `undefined` to callers (CR minor — comment was claiming a fallback that didn't actually happen). - `config/schemas.rs`: add structured `log::debug!" entry / exit / error logs to `handle_update_meet_settings` and `handle_get_meet_settings` for production troubleshooting (CR major refactor). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r diff coverage gate Coverage Gate failed at 52% on diff (≥80% required). Untested lines were the new meet RPC bindings and the optimistic-commit setter: - `utils/tauriCommands/config.test.ts`: 4 new cases — both bindings exercise the Tauri-not-available throw path and the happy POST using canonical method names `openhuman.config_update_meet_settings" / `openhuman.config_get_meet_settings". - `providers/__tests__/CoreStateProvider.test.tsx": 2 new cases for `setMeetAutoOrchestratorHandoff` covering both the success path and the post-RPC `refresh()" failure path (which is intentionally swallowed so toggle UX remains responsive on transient core errors). - `test/setup.ts`: extend the global `tauriCommands` mock with `openhumanUpdateMeetSettings` / `openhumanGetMeetSettings` defaults so any future test importing CoreStateProvider gets a working stub. Targeted vitest sweep: 1565 passed / 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
app/src/providers/__tests__/CoreStateProvider.test.tsx (1)
247-277: ⚡ Quick winAssert the state outcome here, not just the RPC call.
At Line 247 the test name says the snapshot flips optimistically, but the assertions only confirm
openhumanUpdateMeetSettingswas called. Add a state assertion (ctx?.snapshot.meetAutoOrchestratorHandoff) with an aligned refresh mock so this catches regressions in the commit path.As per coding guidelines: “Prefer testing behavior over implementation details” in
app/src/**/*.test.{ts,tsx}.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/providers/__tests__/CoreStateProvider.test.tsx` around lines 247 - 277, The test should assert the optimistic state flip: after calling ctx!.setMeetAutoOrchestratorHandoff(true) assert ctx?.snapshot.meetAutoOrchestratorHandoff is true, and also update the snapshot/refresh mock so subsequent reads reflect the new value (e.g. set fetchSnapshot/mock for refresh to return makeSnapshot(..., meetAutoOrchestratorHandoff: true)) so the optimistic flip is validated in addition to verifying tauriCommands.openhumanUpdateMeetSettings was called; update the test around setMeetAutoOrchestratorHandoff, ctx, fetchSnapshot, makeSnapshot and openhumanUpdateMeetSettings accordingly.app/src/services/coreStateApi.ts (1)
24-43: ⚡ Quick winMake the normalized field non-optional in the returned snapshot contract.
At Line 42 the field is still optional, but Lines 63-66 guarantee a boolean at runtime. Consider splitting wire type vs normalized return type so callers get a strict
booleancontract and don’t keep adding redundant?? false.Proposed refactor
-interface AppStateSnapshotResult { +interface AppStateSnapshotWireResult { auth: { @@ - meetAutoOrchestratorHandoff?: boolean; + meetAutoOrchestratorHandoff?: boolean; @@ } -export const fetchCoreAppSnapshot = async (): Promise<AppStateSnapshotResult> => { - const response = await callCoreRpc<{ result: AppStateSnapshotResult }>({ +export interface AppStateSnapshotResult extends Omit<AppStateSnapshotWireResult, 'meetAutoOrchestratorHandoff'> { + meetAutoOrchestratorHandoff: boolean; +} + +export const fetchCoreAppSnapshot = async (): Promise<AppStateSnapshotResult> => { + const response = await callCoreRpc<{ result: AppStateSnapshotWireResult }>({ method: 'openhuman.app_state_snapshot', }); @@ return { ...response.result, meetAutoOrchestratorHandoff: response.result.meetAutoOrchestratorHandoff ?? false, }; };Also applies to: 56-66
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/services/coreStateApi.ts` around lines 24 - 43, The AppStateSnapshotResult type currently marks meetAutoOrchestratorHandoff as optional even though fetchCoreAppSnapshot normalises it to a boolean; update the type contract by introducing a separate wire type (e.g., AppStateSnapshotWire) that keeps meetAutoOrchestratorHandoff optional, and change AppStateSnapshotResult (the function return type used by fetchCoreAppSnapshot) to have meetAutoOrchestratorHandoff: boolean (non-optional); adjust any references to use the new wire type for parsing and ensure fetchCoreAppSnapshot returns the normalized AppStateSnapshotResult so callers no longer need `?? false` (also apply the same split/fix to the other analogous block referenced around the other occurrence).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@app/src/providers/__tests__/CoreStateProvider.test.tsx`:
- Around line 247-277: The test should assert the optimistic state flip: after
calling ctx!.setMeetAutoOrchestratorHandoff(true) assert
ctx?.snapshot.meetAutoOrchestratorHandoff is true, and also update the
snapshot/refresh mock so subsequent reads reflect the new value (e.g. set
fetchSnapshot/mock for refresh to return makeSnapshot(...,
meetAutoOrchestratorHandoff: true)) so the optimistic flip is validated in
addition to verifying tauriCommands.openhumanUpdateMeetSettings was called;
update the test around setMeetAutoOrchestratorHandoff, ctx, fetchSnapshot,
makeSnapshot and openhumanUpdateMeetSettings accordingly.
In `@app/src/services/coreStateApi.ts`:
- Around line 24-43: The AppStateSnapshotResult type currently marks
meetAutoOrchestratorHandoff as optional even though fetchCoreAppSnapshot
normalises it to a boolean; update the type contract by introducing a separate
wire type (e.g., AppStateSnapshotWire) that keeps meetAutoOrchestratorHandoff
optional, and change AppStateSnapshotResult (the function return type used by
fetchCoreAppSnapshot) to have meetAutoOrchestratorHandoff: boolean
(non-optional); adjust any references to use the new wire type for parsing and
ensure fetchCoreAppSnapshot returns the normalized AppStateSnapshotResult so
callers no longer need `?? false` (also apply the same split/fix to the other
analogous block referenced around the other occurrence).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 68e3be55-156e-40bd-9899-95ee822b061a
📒 Files selected for processing (6)
app/src/components/settings/panels/PrivacyPanel.tsxapp/src/providers/__tests__/CoreStateProvider.test.tsxapp/src/services/coreStateApi.tsapp/src/test/setup.tsapp/src/utils/tauriCommands/config.test.tssrc/openhuman/config/schemas.rs
…nsai#1299) (tinyhumansai#1310) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
meet.auto_orchestrator_handoffconfig (defaultfalse) plusupdate_meet_settings/get_meet_settingsJSON-RPC controllers.meetAutoOrchestratorHandoffinAppStateSnapshotand pipe it throughCoreStateProviderwith asetMeetAutoOrchestratorHandoffsetter.flushMeetingSession→handoffToOrchestratoron the new flag (read fresh permeet_call_ended, fail-closed on RPC error).Problem
flushMeetingSessionunconditionally calledhandoffToOrchestratorwhenever a Google Meet call ended with at least one caption segment. The handoff opens a fresh chat thread and feeds the orchestrator a prompt explicitly inviting tool execution ("proactively handle it now"). Because the orchestrator has the full Slack tool surface, it routinely posted meeting summaries to#generalwithout user consent — workspace-wide leak risk for sensitive meeting content. The hole has shipped since PR #629 landedwebviewAccountService.ts.Solution
Layer 1 of the issue's proposed remediation (the smallest immediately-correct fix). All other layers (draft-by-default prompt, channel allow-list, tool-layer approval gate) are explicitly out of scope for this PR.
MeetConfig { auto_orchestrator_handoff: bool }mirrorsObservabilityConfigshape; same patch struct +apply_meet_settings+ load-and-apply ops; registered through the existingconfigcontroller catalog asopenhuman.config_update_meet_settings/openhuman.config_get_meet_settings. Surfaced onAppStateSnapshotasmeetAutoOrchestratorHandoffso the React shell can read the value alongsideanalyticsEnabledetc.utils/tauriCommands/config.ts;CoreAppSnapshotextended with the required field (emptySnapshotdefaultsfalse);CoreStateProvider.setMeetAutoOrchestratorHandoffmirrors the optimistic-commit + refresh pattern ofsetAnalyticsEnabled.webviewAccountService.tswraps the existinghandoffToOrchestratorcall in a newmaybeHandoffToOrchestratorthat readsopenhuman.config_get_meet_settingspermeet_call_ended(rare event; ~30ms; always-current), only invokes the handoff when the flag is explicitlytrue, and fails closed on RPC error. Memory ingest at line 594 is unchanged: transcripts still land in memory, only the auto-action path is gated.PrivacyPanelreusing the analytics toggle markup. Copy explains the tradeoff verbatim ("...may take actions like drafting messages, scheduling follow-ups, or posting summaries to your connected Slack workspace. Off by default.").Smoked end-to-end on a live Gmeet call with caption capture: with toggle OFF the orchestrator thread is not created; with toggle ON the orchestrator session is built (
thread-12c9ad69-…, 7 delegation tools, 21 tool specs).Submission Checklist
docs/TESTING-STRATEGY.mddiff-cover) meet the gate enforced by.github/workflows/coverage.yml. Runpnpm test:coverageandpnpm test:rustlocally; PRs below 80% on changed lines will not merge.## Related— same reason as above.docs/TESTING-STRATEGY.md)Closes #NNNin the## RelatedsectionImpact
#generaland other Slack channels are no longer reachable from the meet-end path without explicit opt-in.config_get_meet_settingsRPC errors (privacy-conservative).Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/1299-meet-handoff-consent-gateTooling
mcp__sequential-thinking__sequentialthinkingper workspace Phase 2 protocol; user-approved before implementation.Summary by CodeRabbit
New Features
Tests