Skip to content

feat(privacy): gate runs on AI opt in org setting#645

Merged
sarahxsanders merged 15 commits into
mainfrom
feat/ai-opt-in-gate
Jun 15, 2026
Merged

feat(privacy): gate runs on AI opt in org setting#645
sarahxsanders merged 15 commits into
mainfrom
feat/ai-opt-in-gate

Conversation

@sarahxsanders

@sarahxsanders sarahxsanders commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

review in graphite if easier (stacked): https://app.graphite.com/github/pr/PostHog/wizard/644/feat(privacy)-consolidate-disclosure-into-Privacy-%26-data-usage-panel

gates wizard runs on AI opt in org setting. there's three behaviors:

  • opted in already: wizard runs like normal
  • not opted in (admin perms): asked to enable the setting for third party providers (since we use claude) w/ easy open controls
  • no opted in (member perms): prompted to ask their org admin to turn it on

mirrors Max's strict reading from in-app: null, undefined, false all block. only literal true proceeds. we'll want to pay attention to usage, so I've added some tracking to this to see if we get spikes: https://us.posthog.com/project/2/dashboard/1701446

programs that don't run the agent (doctor, mcp add/remove/tutorial, upload-source-maps) opt out via requiresAi: false on ProgramConfig and skip the gate entirely!!!

CI

treated as implicit consent

demos

AI opt in screen (with admin perms):
CleanShot 2026-06-11 at 15 47 57@2x

AI opt in screen (w/o admin perms):
CleanShot 2026-06-11 at 15 48 34@2x

testing

pnpm try --playground and tab to AI opt-in (admin) / AI opt-in (non-admin)

sarahxsanders commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

@github-actions

Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci basic-integration
  • /wizard-ci error-tracking-upload-source-maps
  • /wizard-ci misc
  • /wizard-ci revenue

Test an individual app:

  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
Show more apps
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci error-tracking-upload-source-maps/android
  • /wizard-ci error-tracking-upload-source-maps/flutter
  • /wizard-ci error-tracking-upload-source-maps/ios
  • /wizard-ci error-tracking-upload-source-maps/next
  • /wizard-ci error-tracking-upload-source-maps/next-no-posthog
  • /wizard-ci error-tracking-upload-source-maps/node-raw
  • /wizard-ci error-tracking-upload-source-maps/node-rollup
  • /wizard-ci error-tracking-upload-source-maps/node-rollup-typescript-plugin
  • /wizard-ci error-tracking-upload-source-maps/node-webpack
  • /wizard-ci error-tracking-upload-source-maps/nuxt-3-6
  • /wizard-ci error-tracking-upload-source-maps/nuxt-4-3
  • /wizard-ci error-tracking-upload-source-maps/react-native
  • /wizard-ci error-tracking-upload-source-maps/react-vite
  • /wizard-ci error-tracking-upload-source-maps/rust
  • /wizard-ci misc/quack-quack
  • /wizard-ci revenue/stripe

Results will be posted here when complete.

@sarahxsanders sarahxsanders changed the base branch from feat/privacy-disclosure to graphite-base/645 June 11, 2026 17:22
@sarahxsanders sarahxsanders changed the base branch from graphite-base/645 to feat/privacy-disclosure June 11, 2026 17:22
@sarahxsanders sarahxsanders force-pushed the feat/ai-opt-in-gate branch 7 times, most recently from eb765e7 to ec2e602 Compare June 11, 2026 19:43
@sarahxsanders sarahxsanders changed the title feat(privacy): gate runs on organization is_ai_data_processing_approved feat(privacy): gate runs on AI opt in org setting Jun 11, 2026
@sarahxsanders sarahxsanders marked this pull request as ready for review June 11, 2026 20:51
@sarahxsanders sarahxsanders requested a review from a team June 11, 2026 20:52

@gewenyu99 gewenyu99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments I think you should consider, but thank you for figuring out this weird shape!

Comment thread src/ui/tui/screen-sequences.ts Outdated
Comment thread src/ui/tui/screen-sequences.ts
Comment thread src/ui/tui/screens/AiOptInRequiredScreen.tsx
Comment thread src/ui/tui/screens/AiOptInRequiredScreen.tsx
Comment thread src/ui/tui/screens/AiOptInRequiredScreen.tsx
Comment thread src/ui/tui/screens/AiOptInRequiredScreen.tsx
Comment thread src/ui/tui/screens/AiOptInRequiredScreen.tsx
@sarahxsanders sarahxsanders force-pushed the feat/ai-opt-in-gate branch 4 times, most recently from 2db086f to ff7cbb7 Compare June 12, 2026 20:32
@sarahxsanders sarahxsanders force-pushed the feat/privacy-disclosure branch from 943b995 to b5f58f7 Compare June 12, 2026 20:32
Base automatically changed from feat/privacy-disclosure to main June 15, 2026 16:52
sarahxsanders and others added 4 commits June 15, 2026 12:57
Honors the same AI opt-in toggle Max gates on. When the wizard
authenticates against an org whose is_ai_data_processing_approved is not
true, AiOptInRequiredScreen renders before the agent starts. Admins get
[O] open-settings; non-admins see a copyable link to share with their
admin. Both variants offer [S] BYOAI skill, [R] retry without
restarting, and [E] exit.

Gate plumbing: screen-sequences.ts injects an ai-opt-in ProgramStep
after the auth step for any program whose requiresAi !== false. Strict
reading matches Max (only literal true proceeds); apiUser=null is
treated as "fetch hasn't happened yet" to avoid flashing the gate
during the brief window between setCredentials and setApiUser emits.

Predicate coverage: tests in programs.test.ts cover all four field
states (true / false / null / undefined), the null-apiUser transient,
and confirm requiresAi: false programs (doctor) have no gate injected.

README adds a Privacy & data usage section and documents --no-telemetry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mount the real screen against a synthetic WizardStore pre-populated
with apiUser + credentials so the admin (membership_level >= 8) and
non-admin variants render without needing real PostHog accounts with
the toggle flipped off. Two tabs share one demo function.

Run with: pnpm try --playground, then arrow-key over to
"AI opt-in (admin)" / "AI opt-in (non-admin)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous URL was {cloud}/settings/organization-ai-consent which 404s.
Real path is {cloud}/project/{id}/settings/organization-details with
#organization-ai-consent as a fragment that scrolls to the toggle.

Pulls projectId off session.credentials. Falls back to a project-less
URL if it's somehow missing post-auth (defensive only — PostHog routes
to the user's default project in that case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues observed in the playground demo:

1. Pressing [S] expanded a 4-row "Prefer your own AI? / Skill: / URL:"
   block that pushed the [O] action off the visible viewport in admin
   mode. Collapse to one inline line matching the PrivacyPanel pattern
   ("Prefer your own AI? Download the skill: <url>"). Saves ~3 rows.

2. The settings URL ternary used `projectId ? ... : ...` which treats
   0 as falsy and routed playground demo sessions (projectId = 0) to
   the project-less fallback. Switch to `projectId != null` so 0 still
   builds the project-scoped URL.

Drops the now-unused SkillSourceInfo import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sarahxsanders and others added 10 commits June 15, 2026 12:57
…edScreen

The playground's TabContainer eats ~5 rows of chrome (tab bar + status
bar + demo header), which makes it impossible to see what the screen
actually looks like in production at default Terminal sizes.

scripts/preview-ai-opt-in.tsx mounts the real AiOptInRequiredScreen
against a synthetic store at full terminal height with no wrapper.

Usage:
  pnpm preview:ai-opt-in admin       # admin variant
  pnpm preview:ai-opt-in non-admin   # non-admin variant

Keys on the screen are LIVE — same caveat as the playground demo
(Ctrl-C to exit, [E] also exits, [O] opens a real browser tab).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the settings URL was built from getCloudUrlFromRegion(),
which hardcodes either us.posthog.com or eu.posthog.com based on the
local session.region. If session.region is wrong, stale, or somehow
missing, the user lands on the wrong region's domain.

Switch to https://app.posthog.com/... which PostHog redirects to the
user's actual region server-side based on their signed-in profile.
This is the standard convention for share-with-user links.

Adds POSTHOG_APP_URL constant alongside DEFAULT_URL. Keeps
getCloudUrlFromRegion in use for the retry's fetchUserData call, which
still needs to hit the user's region-specific API server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/preview-ai-opt-in.tsx and the pnpm preview:ai-opt-in script
were a local-testing convenience. The AiOptInDemo in the playground
covers the same need for anyone else who needs to preview the screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI runs already auto-consent to AI usage per the README's "When running
in CI mode" notes, and the gate's interactive [O]/[R]/[E] flow would
be unworkable in a headless context anyway. Skip the gate when
session.ci is true.

show: returns false in CI so the router walks past the step.
isComplete: returns true in CI so the predicate is consistent (defensive
— show=false already skips it).

New test case covers the CI bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…zation:read

Two prod-blocking fixes found while investigating why the gate didn't
work in real runs:

1. The gate was cosmetic. The router rendered AiOptInRequiredScreen,
   but runAgent performs OAuth internally and immediately proceeded to
   skill install + agent start — the wizard log showed "starting OAuth"
   to "agent initialized" in 11s with no pause. Source reached Claude
   while the kill screen was up.

   Fix: move withAiOptInGate to src/lib/programs/ai-opt-in-gate.ts and
   give the injected step a real `gate` predicate. The store registers
   it in _initFromProgram (same wrapper screen-sequences.ts uses, so
   screen and gate can't drift). New WizardUI.waitForAiOptIn() —
   ink-ui delegates to store.getGate('ai-opt-in'); logging-ui resolves
   immediately (non-TUI = CI = auto-consent). The agent runner awaits
   it after setApiUser and BEFORE skill install, so the run physically
   parks at the kill screen until [R]etry sees approval or the user
   exits. Programs with requiresAi: false never register the gate, so
   the await flows straight through.

2. OAuth-scoped /api/users/@me/ responses omit the org's AI consent
   field without organization:read. Since the gate treats a missing
   field as not-opted-in (Max's strict reading), every org — including
   opted-in ones — would have been blocked. Add organization:read to
   WIZARD_OAUTH_SCOPES.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ope ceiling

The wizard's OAuth application has a server-side scope ceiling
(OAuthApplication.scopes in posthog); requesting any scope outside it
fails the entire authorize request with error=invalid_scope before the
consent screen renders. organization:read isn't in the wizard app's
ceiling, so adding it broke ALL wizard auth.

Replace it with a comment documenting the ceiling constraint so the
next scope addition gets coordinated with the posthog side first.

Whether the gate actually needs organization:read is unconfirmed — the
/api/users/@me/ serializer nests the full OrganizationSerializer
(is_ai_data_processing_approved included) with no scope-based field
filtering visible in posthog source. Verifying empirically with a real
OAuth run before requesting any ceiling change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… URL

Same copy/paste-corruption fix as the PrivacyPanel: the ~89-char
tarball URL hard-wraps in the terminal and breaks when pasted. Name
the resolved skill in the sentence and link the one-line release page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-adds the scope reverted in fb717f2. The wizard OAuth application's
server-side scope ceiling now includes organization:read in both prod
regions (Django admin edit per the scope-ceiling-invalid-scope runbook
in PostHog/runbooks), so the authorize request no longer fails with
invalid_scope.

With this scope, /api/users/@me/ returns
organization.is_ai_data_processing_approved and the AI opt-in gate can
read the field it enforces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The _initFromProgram doc-comment conflict resolution raced with gt
continue and the markers landed in commit a0a304d. Merges main's
accurate description (onInit moved to runInitHooks) with the
withAiOptInGate note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AI opt-in gate reads organization.is_ai_data_processing_approved
from /api/users/@me/, which needs organization:read on OAuth tokens.
This list is the copy-paste source for the Django admin Scopes field
(both prod regions) — update the admin from THIS list per the
scope-ceiling-invalid-scope runbook in PostHog/runbooks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sarahxsanders sarahxsanders merged commit 6cbf743 into main Jun 15, 2026
18 checks passed
@sarahxsanders sarahxsanders deleted the feat/ai-opt-in-gate branch June 15, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants