Skip to content

feat(sdk): add the Platform API and managed-user mode - #6

Draft
a7vinx wants to merge 7 commits into
mainfrom
feat/platform-managed-user
Draft

a7vinx wants to merge 7 commits into
mainfrom
feat/platform-managed-user

Conversation

@a7vinx

@a7vinx a7vinx commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Pine's B2B Platform API lets an enterprise (tenant) hold a secret key (pine_sk_live_... / pine_sk_test_...) and create managed users identified by its own external_id. This PR makes the SDK the complete enterprise integration: create the managed user, then act as them over REST and Socket.IO.

Public API

  • AsyncPineAI(api_key=...) / PineAI(api_key=...) is a tenant client. Keys must start with pine_sk_live_ or pine_sk_test_.
    • client.platform.managed_users.create(external_id, *, email, name, phone=None) is idempotent: 201 on create, 200 with the stored user unchanged afterwards.
    • client.platform.managed_users.get(external_id). Here the external_id goes into the path by design: it is validated first, then percent-encoded.
    • Both return a typed ManagedUser (id, external_id, email, name, phone, created_at) and raise PlatformError with a stable code and status_code, never the upstream body.
    • Platform calls never send Pine-Managed-User.
  • AsyncPineAI(api_key=..., managed_user=external_id) acts as that managed user.
    • REST requests send Authorization: Bearer <key> and Pine-Managed-User, and the Socket.IO handshake sends {"token": key, "managed_user": external_id}.
    • connect() resolves the Pine user ID that envelopes need, once per client, through auth.me() (GET /api/v2/auth/me). No external_id goes into a URL path in managed-user mode.
    • The managed user is bound to its API key and is never sent with any other token, such as one installed by set_token()/auth.verify_code() or a per-request token=.
  • client_name="..." sends Pine-Client on REST requests and on the Socket.IO handshake. The backend records it only for tenant-key requests.
  • Identity headers (Authorization, Pine-Managed-User, Pine-Client) are applied after an injected client's defaults and after caller headers=, so neither can override them.
  • New public names: API_KEY_PREFIXES, validate_external_id, MANAGED_USER_HEADER, CLIENT_HEADER, PlatformAPI, ManagedUsersAPI (and their sync variants), ManagedUser and PlatformError.
  • Invalid configurations raise ValueError without echoing the key or the external_id.

The README gains "Integrating as an enterprise (Platform API)", and the CHANGELOG gains an Unreleased entry. Versions are bumped only in release commits (RELEASING.md), so the version is unchanged here.

Backend contract

  • RunVid/webapp-backend#2024 adds email, name and phone to the managed user, key + Pine-Managed-User auth on /api/v2 (including /api/v2/auth/me), and the /api/v2/socket.io/ handshake. Where it differs from RunVid/webapp-backend#2022, this PR follows #2024.
  • The current-user lookup is GET /api/v2/auth/me. GET /v2/users is not a backend route.

Rollout order (canonical — identical in all Platform API PRs)

  1. Deploy email-service #76.
  2. Run the production identity backfill (run 1).
  3. Deploy backend #2024 and Agent1 #1455; tenants.managed_user_access_enabled stays false.
  4. Run the production identity backfill again (run 2).
  5. Deploy email-service #77.
  6. Merge a backend change setting tenants.managed_user_access_enabled: true in the target environment's config (and updating TestCheckedInManagedUserAccessSwitch), deploy, then create tenants.

Constraints: #2022 before #2024; SDK #6 released before MCP #13 merges; MCP deploys after #2024; end-to-end testing of SDK/MCP in staging requires staging's own steps 1–6 first.

Tests

  • tests/test_platform.py runs against a fake backend on 127.0.0.1 (aiohttp + python-socketio). It covers:
    • REST and handshake identity, Pine-Client on the handshake, and a single auth.me() across reconnects;
    • an unchanged user-token handshake;
    • create/get typing, with a fixed stored user on repeat create, and percent-encoding;
    • Platform errors 400/401/403/404/409/429/500, and user-scoped 403 platform_route_not_allowed and 429 on sessions.list(), with no leakage;
    • key binding after set_token, caller headers that cannot override identity, the key-prefix rule, and the public names.
  • The sync client is tested with a mock transport.
  • Every run used an environment with only HOME/PATH and proxies pointed at a dead local port.
ruff check src tests                       -> All checks passed!
pytest tests/ --ignore=tests/integration   -> 173 passed

🤖 Generated with Claude Code

a7vinx and others added 7 commits October 7, 2026 10:35
A tenant client built with api_key can create and get managed users
through client.platform.managed_users, with typed ManagedUser values and
PlatformError failures that never carry upstream bodies. Adding
managed_user acts as that user: REST requests send the key and a
Pine-Managed-User header, the Socket.IO handshake sends token and
managed_user, and connect() resolves the Pine user ID through auth.me().
client_name adds a Pine-Client header to every REST request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
client_name now also goes to the Socket.IO handshake as a Pine-Client
header, where the backend reads it for attribution. The identity headers
become a method of a base shared by both HTTP transports instead of a
module function reading their private fields. The README states that the
backend records Pine-Client only for tenant-key requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The managed-user header is sent only with the API key it was configured
with, never after set_token() or with a per-request token. Identity
headers (Authorization, Pine-Managed-User, Pine-Client) are applied after
caller headers, as on main, so http.* callers cannot override them.
API keys must start with pine_sk_live_ or pine_sk_test_. API_KEY_PREFIXES,
validate_external_id, MANAGED_USER_HEADER and CLIENT_HEADER are exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mint_ticket() always fails with 403 platform_route_not_allowed as
AuthError, and while the backend's access switch is off user-scoped
calls fail with 403 platform_managed_user_access_disabled as the
resource's usual error type. A test pins both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant