feat(privacy): gate runs on AI opt in org setting#645
Merged
Conversation
Collaborator
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
🧙 Wizard CIRun 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:
Test all apps in a directory:
Test an individual app:
Show more apps
Results will be posted here when complete. |
b244f30 to
cc113e4
Compare
1d30d45 to
1186775
Compare
eb765e7 to
ec2e602
Compare
0b1a38a to
657168d
Compare
gewenyu99
approved these changes
Jun 12, 2026
gewenyu99
left a comment
Collaborator
There was a problem hiding this comment.
Comments I think you should consider, but thank you for figuring out this weird shape!
2db086f to
ff7cbb7
Compare
943b995 to
b5f58f7
Compare
7711d84 to
b9491dc
Compare
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>
…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>
b9491dc to
da9d5cc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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:
mirrors Max's strict reading from in-app:
null,undefined,falseall block. only literaltrueproceeds. 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/1701446programs that don't run the agent (doctor, mcp add/remove/tutorial, upload-source-maps) opt out via
requiresAi: falseonProgramConfigand skip the gate entirely!!!CI
treated as implicit consent
demos
AI opt in screen (with admin perms):

AI opt in screen (w/o admin perms):

testing
pnpm try --playgroundand tab to AI opt-in (admin) / AI opt-in (non-admin)