Skip to content

feat(agentic-provisioning): add partner-scoped GitHub grant store and endpoints - #69857

Merged
rafaeelaudibert merged 3 commits into
masterfrom
rafa/rafaeelaudibert/agentic-github-grants
Jul 14, 2026
Merged

feat(agentic-provisioning): add partner-scoped GitHub grant store and endpoints#69857
rafaeelaudibert merged 3 commits into
masterfrom
rafa/rafaeelaudibert/agentic-github-grants

Conversation

@rafaeelaudibert

@rafaeelaudibert rafaeelaudibert commented Jul 10, 2026

Copy link
Copy Markdown
Member

Problem

We're exploring a cloudflare.com/drop-style flow on posthog.com: a visitor connects GitHub, picks a repo, and one click later they have a PostHog account and a cloud wizard run opening a PR against their repo.
posthog.com would be registered as an HMAC agentic-provisioning partner, but it runs on Vercel and must never hold GitHub tokens or the GitHub App client secret.

This PR is part 1 of a 3-PR stack: it gives partners a way to hand us a GitHub OAuth code and get back an opaque handle, with all token custody staying inside PostHog.

Changes

  • New GitHub grant store (ee/api/agentic_provisioning/github_grants.py): exchanges the code via the existing GitHubIntegration.github_user_from_code, fetches the user's primary verified email from GET /user/emails, and caches the resulting user authorization for 60 minutes, encrypted with the same MultiFernet keys that back EncryptedJSONField, bound to the creating partner. Partners only ever see an opaque grant_id.
  • POST /api/agentic/provisioning/github/grants (HMAC partner auth, provisioning_can_create_accounts gated) returns {grant_id, gh_login, email, expires_in}. email is null when the GitHub user has no verified email — the grant is still created and the partner collects an email inline. Only an access refusal on GET /user/emails (App permission misconfiguration) hard-fails, with email_unavailable (502), so a permission regression stays loud instead of degrading every drop into inline-email mode.
  • GET /api/agentic/provisioning/github/grants/:id/repositories lists the user's installations of our GitHub App and the repos each grants, using the stored user token. Only the creating partner can read its grants; a wrong-partner read looks identical to a missing grant so the endpoint can't be used as an existence oracle. Pollable while the visitor installs the App in another tab, with a per-grant budget.
  • New github_grants partner rate-limit bucket (10/h default) plus a provisioning_rate_limit_github_grants override field on OAuthApplication (nullable AddField, safe).
  • Both endpoints registered on the api/agentic/... and api/provisioning/... URL trees. No region proxy: grants are region-local by design and v1 is US-only (documented in the module docstring).

Note: user-to-server tokens only ever see installations of the App that minted them, so no app-slug filtering is needed when listing installations (same as the existing personal_finish.py flow).

How did you test this code?

Added test_github_grants.py (15 tests). The regressions they guard: forgetting to thread redirect_uri into the code exchange (GitHub rejects mismatches), storing tokens unencrypted in Redis (asserts the raw cache value doesn't contain the token), cross-partner grant reads (existence-oracle guard), the no-verified-email → email: null grant path, the permission-refusal → email_unavailable hard error, failed-exchange error mapping, and both rate limiters. Also extended test_url_aliases.py for the alias tree. Ran the full ee/api/agentic_provisioning/ suite (398 passed) plus hogli ci:preflight.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

None: partner-facing provisioning endpoints aren't publicly documented yet.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code implemented this from an RFC written by the assignee, working autonomously overnight. Skills invoked: /improving-drf-endpoints, /django-migrations, /writing-tests. Notable decisions: encrypted the cached grant payload (no prior precedent for encrypting cache values, but these are GitHub user tokens), folded partner-mismatch into grant_not_found to avoid an existence oracle, and made the RFC correction that github_user_from_code never fetched emails, so the grant endpoint fetches GET /user/emails itself (requires the App's "Email addresses (read)" permission, which is an ops prerequisite).

… endpoints

Adds a server-side GitHub grant store to the agentic provisioning API so a
partner (e.g. posthog.com) can exchange a GitHub OAuth code without ever
holding GitHub tokens: the tokens are encrypted and cached region-locally
under an opaque grant_id bound to the creating partner.

- POST /api/agentic/provisioning/github/grants exchanges the code, reads the
  user's primary verified email, and returns {grant_id, gh_login, email}
- GET /api/agentic/provisioning/github/grants/:id/repositories lists the
  user's installations of our GitHub App and their repos, for the repo picker
  (pollable while the visitor installs the App, with a per-grant budget)
- new github_grants partner rate-limit bucket + OAuthApplication override field

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Migration SQL Changes

Hey 👋, we've detected some migrations on this PR. Here's the SQL output for each migration, make sure they make sense:

posthog/migrations/1253_oauth_application_github_grants_rate_limit.py

BEGIN;
--
-- Add field provisioning_rate_limit_github_grants to oauthapplication
--
ALTER TABLE "posthog_oauthapplication" ADD COLUMN "provisioning_rate_limit_github_grants" integer NULL;
COMMIT;

Last updated: 2026-07-14 03:22 UTC (0022a7c)

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🔍 Migration Risk Analysis

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 0 Needs Review | 0 Blocked

✅ Safe

Brief or no lock, backwards compatible

posthog.1253_oauth_application_github_grants_rate_limit
  └─ #1 ✅ AddField
     Adding nullable field requires brief lock
     model: oauthapplication, field: provisioning_rate_limit_github_grants

📚 How to Deploy These Changes Safely

AddField:

This operation acquires a brief lock but doesn't rewrite the table.

Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up.

Last updated: 2026-07-14 03:23 UTC (0022a7c)

@trunk-io

trunk-io Bot commented Jul 10, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/rafaeelaudibert/agentic-github-grants branch 2 times, most recently from 3873684 to ee5d7c1 Compare July 10, 2026 16:36
@rafaeelaudibert
rafaeelaudibert marked this pull request as ready for review July 10, 2026 18:45
@graphite-app graphite-app Bot added the stamphog Request AI approval (no full review) label Jul 10, 2026
@rafaeelaudibert
rafaeelaudibert requested review from a team, MattBro and fercgomes and removed request for a team July 10, 2026 18:46
Comment thread ee/api/agentic_provisioning/views.py
Comment thread ee/api/agentic_provisioning/views.py
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(agentic-provisioning): add partner-..." | Re-trigger Greptile

Comment thread ee/api/agentic_provisioning/github_grants.py
Comment thread ee/api/agentic_provisioning/github_grants.py
Comment thread ee/api/agentic_provisioning/views.py Outdated
Comment thread ee/api/agentic_provisioning/views.py
@veria-ai

veria-ai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request adds a partner-scoped GitHub grant store and related API endpoints for agentic provisioning. The touched GitHub grant flow lists GitHub App installations and repositories associated with a grant.

There is one open security concern remaining after two issues have been addressed. The remaining issue is an authenticated resource-amplification path where listing GitHub installations can trigger a large number of outbound GitHub repository requests per poll, creating avoidable load and potential rate-limit pressure. The risk appears bounded to authenticated use of this endpoint rather than direct data exposure or privilege bypass.

Open issues (1)

Fixed/addressed: 2 · PR risk: 5/10

rafaeelaudibert and others added 2 commits July 10, 2026 16:06
…b errors

Restrict GitHub grant endpoints to proof-bearing (hmac/bearer) partners so a
public PKCE client_id can't exchange codes or read grant data, and map
github_user_from_code request/JSON failures to github_unavailable.

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

Lands the send_provisioning_welcome repository kwarg (and its template paragraph)
ahead of the account_requests caller that passes it, so a rolling deploy never
hands the new kwarg to an old Celery worker. The caller ships in a later PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/rafaeelaudibert/agentic-github-grants branch from ee5d7c1 to a21d787 Compare July 10, 2026 19:32
@stamphog

stamphog Bot commented Jul 10, 2026

Copy link
Copy Markdown

Note

🤖 stamphog reviewed a21d7875bed60111a23be6f3c9070f330c1844ca — verdict: REFUSED

Gates denied this PR (deny-list hit on auth/migrations, and it's classified T2-never) so it cannot be auto-approved. It's squarely in risky territory (GitHub token custody, new partner-facing auth endpoints, DB migration), and while the author says they fixed several high/medium severity findings from security bots, those bot reviews are all on an older commit — no bot or human has re-reviewed the current head to confirm the fixes are correct.

  • rafaeelaudibert reviewed the current head.
  • Gate verdict is DENIED (deny-list: auth, migrations; tier: T2-never) — outside auto-approval scope by policy
  • Security-sensitive changes (GitHub OAuth token custody, partner auth) with multiple high/medium severity findings from hex-security-app and veria-ai bots, all on an older commit (ee5d7c1) — no independent re-review on the current head confirming the fixes actually close the gaps
  • Author (rafaeelaudibert) is not on the owning team @PostHog/team-platform-features, removing the owning-team assurance path
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list matches: auth, migrations
size 531L, 8F substantive, 855L/12F incl. docs/generated/snapshots — within ceiling
tier classified as T2-never: T2-never (855L, 12F, two-areas, feat)
stamphog 2.0.0b3 .stamphog/policy.yml @ 79e362f · reviewed head a21d787

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Jul 10, 2026
@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/rafaeelaudibert/agentic-github-grants branch from a21d787 to a57ad91 Compare July 10, 2026 19:36
Comment thread ee/api/agentic_provisioning/views.py
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Playwright — all passed

All tests passed.

View test results →

⚠️ Backend coverage — 92.0% of changed backend lines covered — 25 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ██████████████████░░ 92.0% (328 / 353)

File Patch Uncovered changed lines
ee/api/agentic_provisioning/github_grants.py 86.7% 70, 136, 143–145, 153, 174, 180, 186, 219, 223, 249, 254, 259, 262
ee/api/agentic_provisioning/views.py 88.6% 825–826, 855, 922–924, 947–948, 970, 974

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29303254102 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.5% 8,345 / 21,104
demo ███████████░░░░░░░░░ 55.2% 1,436 / 2,601
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks █████████████░░░░░░░ 66.7% 24,376 / 36,570
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
data_modeling ████████████████░░░░ 78.4% 4,696 / 5,987
signals ████████████████░░░░ 78.5% 18,074 / 23,028
cdp ████████████████░░░░ 80.6% 3,105 / 3,851
wizard ████████████████░░░░ 82.5% 772 / 936
cohorts █████████████████░░░ 82.9% 3,048 / 3,675
notebooks █████████████████░░░ 83.8% 6,086 / 7,259
agent_platform █████████████████░░░ 84.1% 3,095 / 3,678
actions █████████████████░░░ 86.6% 717 / 828
engineering_analytics █████████████████░░░ 87.3% 4,269 / 4,889
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.3% 6,857 / 7,763
visual_review ██████████████████░░ 88.5% 5,565 / 6,287
business_knowledge ██████████████████░░ 88.5% 4,400 / 4,969
conversations ██████████████████░░ 88.9% 15,924 / 17,921
mcp_analytics ██████████████████░░ 89.1% 2,485 / 2,790
dashboards ██████████████████░░ 89.1% 5,650 / 6,342
error_tracking ██████████████████░░ 89.2% 9,473 / 10,615
streamlit_apps ██████████████████░░ 90.4% 2,499 / 2,764
slack_app ██████████████████░░ 90.6% 9,460 / 10,444
links ██████████████████░░ 90.6% 183 / 202
marketing_analytics ██████████████████░░ 90.7% 11,476 / 12,646
product_analytics ██████████████████░░ 91.2% 5,621 / 6,164
managed_migrations ██████████████████░░ 91.9% 908 / 988
workflows ██████████████████░░ 92.0% 4,795 / 5,210
mcp_store ██████████████████░░ 92.1% 3,665 / 3,981
data_warehouse ██████████████████░░ 92.1% 17,282 / 18,765
alerts ██████████████████░░ 92.1% 3,389 / 3,678
web_analytics ███████████████████░ 92.7% 13,702 / 14,787
notifications ███████████████████░ 92.7% 1,026 / 1,107
ai_observability ███████████████████░ 92.7% 14,670 / 15,822
surveys ███████████████████░ 92.9% 5,660 / 6,094
posthog_ai ███████████████████░ 93.2% 1,311 / 1,407
tracing ███████████████████░ 93.2% 2,423 / 2,599
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
early_access_features ███████████████████░ 93.8% 848 / 904
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,606 / 9,143
messaging ███████████████████░ 94.3% 2,366 / 2,508
skills ███████████████████░ 94.4% 2,819 / 2,987
revenue_analytics ███████████████████░ 94.4% 3,586 / 3,797
growth ███████████████████░ 94.9% 2,393 / 2,522
logs ███████████████████░ 95.3% 9,429 / 9,895
experiments ███████████████████░ 95.6% 24,057 / 25,166
replay_vision ███████████████████░ 95.6% 12,690 / 13,272
feature_flags ███████████████████░ 96.1% 14,368 / 14,957
warehouse_sources ███████████████████░ 96.1% 211,729 / 220,375
annotations ███████████████████░ 96.2% 732 / 761
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
access_control ███████████████████░ 96.8% 849 / 877
customer_analytics ███████████████████░ 97.3% 7,277 / 7,481
data_catalog ███████████████████░ 97.3% 1,288 / 1,324
analytics_platform ████████████████████ 98.2% 2,098 / 2,137
metrics ████████████████████ 98.3% 2,363 / 2,403
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

</p>
{% if repository %}
<p>
We're setting up PostHog in <strong>{{ repository }}</strong> in the background — a pull request will appear on the repository shortly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 freeze, you've been pulled over by the em-dash police

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This one is actually right tho, this should be an em-dash lol

@@ -2,6 +2,11 @@
<p>
Welcome to PostHog! Your account was created{% if partner_name %} via {{ partner_name }}{% endif %}.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

make sure to update this on customer.io

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done

@fercgomes fercgomes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'm still a bit confused on how this works, but i'm not blocking it

# ---------------------------------------------------------------------------
# POST /provisioning/github/grants
# GET /provisioning/github/grants/:grant_id/repositories
# GitHub grants for drop-style partner flows: the partner forwards a GitHub OAuth

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you improve the documentation a bit? I'm still not sure what "drop-style" is, the link in the PR description is broken.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, this is not a good explanation, I'll reword this

It's supposed to mean cloudflare.com/drop where people can just click a button and get something live

if partner is None:
return (
Response(
{"type": "error", "error": {"code": "unauthorized", "message": "Authentication required"}},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel there's a lot of duplication in how we handle responses in the provisioning API

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Absolutely, this should be cleaned up eventually

installations: list[dict[str, Any]] = []
repositories: list[dict[str, Any]] = []
raw_installations = response.json().get("installations", [])
if not isinstance(raw_installations, list):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

doesn't the default value for get already handle this? honest question tho

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not really

{}.get("installations", []) # []
{"installations": None}.get("installations, []) # None

def _list_installation_repositories(access_token: str, installation_id: str) -> list[dict[str, Any]]:
repositories: list[dict[str, Any]] = []
for page in range(1, _REPOSITORY_MAX_PAGES + 1):
response = github_request(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what's the purpose of using a github specific client, if you're passing all the request parameters inline?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was something @webjunkie added. This means we can have a single place controlling rate limits.

"repository_selection": installation.get("repository_selection"),
}
)
repositories.extend(_list_installation_repositories(access_token, installation_id))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i don't this will ever return a considerable amount of installations, but it's wise to be mindful of the fanout pattern here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can someone connect a single App installation more than once? That would be weird. But yeah, agreed we should keep this in mind

@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/rafaeelaudibert/agentic-github-grants branch from 077c216 to 0022a7c Compare July 14, 2026 03:19
"repository_selection": installation.get("repository_selection"),
}
)
repositories.extend(_list_installation_repositories(access_token, installation_id))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium: Unbounded installation fan-out

An authenticated caller can install the GitHub App across many accounts and repeatedly poll this endpoint, causing up to 300 repository requests per poll because all 100 returned installations receive three pages each. The per-grant limit still permits 120 such polls; enforce a small total outbound-request budget across the entire listing operation, or paginate the installations instead of expanding all of them synchronously.

@rafaeelaudibert
rafaeelaudibert merged commit c3d18e8 into master Jul 14, 2026
282 checks passed

Copy link
Copy Markdown
Member Author

Merge activity

@rafaeelaudibert
rafaeelaudibert deleted the rafa/rafaeelaudibert/agentic-github-grants branch July 14, 2026 03:38
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-14 04:15 UTC Run
prod-us ✅ Deployed 2026-07-14 04:25 UTC Run
prod-eu ✅ Deployed 2026-07-14 04:27 UTC Run

rafaeelaudibert added a commit that referenced this pull request Jul 14, 2026
…source actions (#69858)

## Problem

Part 2 of the wizard-drop stack (base: #69857).
After posthog.com provisions an account (or an existing user completes the consent flow), the partner holds a bearer token scoped to a team and a `grant_id` from #69857. It now needs to (a) link the visitor's GitHub installation to that team and (b) kick off a cloud wizard run, without a PostHog session.

## Changes

Two team-scoped resource actions, following the exact `rotate_credentials` shape (bearer + HMAC-if-present, `scoped_teams` authorization, `bearer_lookup` region proxy):

- `POST /api/agentic/provisioning/resources/:team_id/github_integration` with `{grant_id, installation_id}`: validates the grant, proves installation ownership with the grant's stored user token (`verify_user_installation_access`), creates both the team `Integration` and the personal `UserIntegration`, consumes the grant, and marks unclaimed provisioned accounts as onboarding-skipped. Idempotent: a retry after the grant was consumed succeeds if the installation is already linked (`already_linked: true`).
- `POST /api/agentic/provisioning/resources/:team_id/wizard_runs` with `{repository, branch?}`: gates on `WIZARD_CLOUD_RUN_OAUTH_CLIENT_ID` (mirroring the session endpoint), applies per-user budgets equivalent to the session throttles (2/hour, 5/day, cache counters since DRF's `UserRateThrottle` can't run on a session-less request) plus a `wizard_runs` partner bucket, then calls `tasks_facade.create_wizard_cloud_run`.

Supporting changes:

- Extracted a request-free `link_github_installation_for_user` from `create_team_github_integration_from_oauth_code` in `posthog/api/github_callback/team_services.py`. The original function now consumes state, exchanges the code, and delegates. One source of truth for the ownership check. Two `ValidationError`s in the shared path gained explicit codes (`installation_access_denied`, `installation_verify_failed`); the messages users see are unchanged.
- New `OnboardingSkippedReason.PROVISIONED` so provisioned accounts don't get routed into onboarding on first login, applied only to unclaimed accounts (never logged in, no password). The choices change is wrapped in `SeparateDatabaseAndState` since choices emit no SQL, so no DDL touches the hot `posthog_user` table (verified with `sqlmigrate`, shows `-- (no-op)`). The enum value flows into the generated frontend/MCP types.
- `provisioning_rate_limit_wizard_runs` override field on `OAuthApplication` (nullable `AddField`).

## How did you test this code?

Added `test_wizard_resource_actions.py` (14 tests). Regressions guarded: both integration rows created and grant consumed exactly once, onboarding flags applied only to unclaimed accounts, ownership failure and GitHub network failure preserving the grant for retry (403 vs 502 distinction the website branches on), scoped-teams authorization, the idempotent retry path, the availability gate, and the per-user burst budget. The `team_services.py` refactor is covered by the existing suite: `posthog/api/test/test_integration.py` (212 passed). Full `ee/api/agentic_provisioning/` suite: 398 passed.

## Automatic notifications

- [ ] Publish to changelog?
- [ ] Alert Sales and Marketing teams?

## Docs update

None: partner-facing provisioning endpoints aren't publicly documented yet.

## 🤖 Agent context

**Autonomy:** Human-driven (agent-assisted)

Claude Code implemented this from the assignee's RFC, autonomously overnight. Skills invoked: /improving-drf-endpoints, /django-migrations, /writing-tests. Decisions worth noting: grant consumption happens exactly when both integration rows are created (never in `wizard_runs`), so the retry matrix stays clean; the per-user wizard budgets are shared cache keys with the bundled path in the next PR so retries can't double-spend. Migrations were renumbered from 1250/1251 to 1251/1252 via `manage.py rebase_migration` after master took 1250 mid-implementation.
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