Repository navigation
End users of your app: connect, saveKey, disconnect and tenantId #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| from . import discover as _discover | ||
| from . import manage as _manage | ||
| from . import schema as _schema | ||
| from .errors import SwytchcodeError | ||
| from .exec import exec_ as _exec | ||
| from .providers.base import Provider, Tool | ||
|
|
||
|
|
@@ -170,7 +171,14 @@ def execute(self, canonical_id: str, args: dict, **options) -> Any: | |
| final_args["body"] = _strip_empty(final_args["body"]) | ||
| if isinstance(final_args.get("params"), (dict, list)): | ||
| final_args["params"] = _strip_empty(final_args["params"]) | ||
| # Forward exec options (dry_run, raw, allow_raw, cwd, env) to the CLI. | ||
| # A client bound to one end user runs every tool for that user unless | ||
| # the call names another one itself. | ||
| if self._c.tenant_id is not None and options.get("tenant_id") is None: | ||
| options["tenant_id"] = self._c.tenant_id | ||
| if options.get("tenant_label") is None: | ||
| options["tenant_label"] = self._c.tenant_label | ||
|
Comment on lines
+176
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Keep the bound label when a call repeats the bound tenant ID. If a call sets 🤖 Prompt for AI Agents |
||
| # Forward exec options (dry_run, raw, allow_raw, cwd, env, tenant_id, | ||
| # tenant_label) to the CLI. | ||
| return _exec(canonical_id, final_args, **options) | ||
|
|
||
| def _tool(self, cid: str) -> Tool: | ||
|
|
@@ -225,8 +233,31 @@ def _ids(self, toolkits, tools, search) -> list[str]: | |
|
|
||
|
|
||
| class Swytchcode: | ||
| def __init__(self, provider: Provider | None = None): | ||
| def __init__( | ||
| self, | ||
| provider: Provider | None = None, | ||
| *, | ||
| tenant_id: str | None = None, | ||
| tenant_label: str | None = None, | ||
| ): | ||
| """ | ||
| tenant_id binds the client to one end user of your app: every tool it | ||
| runs, including those an AI agent picks, uses that user's connected | ||
| accounts and never yours. Create one client per request, from your | ||
| server's session. tenant_label is how approvers see that end user, for | ||
| example "Alice Smith (alice@acme.com)". | ||
| """ | ||
| self.provider = provider | ||
| if tenant_id is not None: | ||
| tenant_id = tenant_id.strip() | ||
| # An empty id would quietly run the agent's tools on your own account. | ||
| if not tenant_id: | ||
| raise SwytchcodeError("tenant_id must be a non-empty string when set") | ||
| self.tenant_id = tenant_id | ||
| tenant_label = (tenant_label or "").strip() or None | ||
| if tenant_label and not tenant_id: | ||
| raise SwytchcodeError("tenant_label needs tenant_id") | ||
| self.tenant_label = tenant_label | ||
| self.tools = _Tools(self) | ||
|
|
||
| def handle_tool_calls( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| """Connect, save and remove accounts for the end users (tenants) of your app.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from .cli import run_cli | ||
| from .errors import SwytchcodeError | ||
|
|
||
|
|
||
| def _tenant_args(provider: str, tenant_id: str) -> list[str]: | ||
| provider = (provider or "").strip() | ||
| tenant_id = (tenant_id or "").strip() | ||
| if not provider: | ||
| raise SwytchcodeError("provider must be a non-empty string") | ||
| # An empty tenant id would act on the developer's own account. | ||
| if not tenant_id: | ||
| raise SwytchcodeError("tenant_id must be a non-empty string") | ||
| return [provider, "--tenant", tenant_id] | ||
|
|
||
|
|
||
| def connect( | ||
| provider: str, | ||
| tenant_id: str, | ||
| *, | ||
| cwd: str | None = None, | ||
| env: dict[str, str] | None = None, | ||
| ) -> dict: | ||
| """ | ||
| Start an OAuth connection for one end user and return the link they open. | ||
|
|
||
| tenant_id is your own id for the end user, taken from your server's session, | ||
| never from the browser. The account is ready once they finish in the popup; | ||
| the first exec() with this tenant_id picks it up. The provider app used | ||
| (Swytchcode's or your own) is the one you chose with `swytchcode auth connect`. | ||
|
|
||
| Returns {"url": ..., "connected_account_uuid": ...}. | ||
| """ | ||
| out = run_cli( | ||
| ["auth", "connect", *_tenant_args(provider, tenant_id), "--json"], | ||
| cwd=cwd, | ||
| env=env, | ||
| ) | ||
| if not isinstance(out, dict) or not out.get("authorization_url"): | ||
| raise SwytchcodeError( | ||
| f"{provider} does not connect with OAuth; collect the end user's key " | ||
| "in your app and save it with save_key()", | ||
| out, | ||
| ) | ||
| return { | ||
| "url": out["authorization_url"], | ||
| "connected_account_uuid": out.get("connected_account_uuid", ""), | ||
| } | ||
|
|
||
|
|
||
| def save_key( | ||
| provider: str, | ||
| tenant_id: str, | ||
| key: str, | ||
| *, | ||
| cwd: str | None = None, | ||
| env: dict[str, str] | None = None, | ||
| ) -> None: | ||
| """ | ||
| Save an end user's API key for a provider that uses keys. The key is stored | ||
| encrypted on this machine only and never sent to Swytchcode; the end user is | ||
| recorded (without the key) toward your plan's end-user limit, so this needs a | ||
| login or API key, and at the limit it raises and nothing is saved. | ||
| """ | ||
| key = (key or "").strip() | ||
| if not key: | ||
| raise SwytchcodeError("key must be a non-empty string") | ||
| out = run_cli( | ||
| ["auth", "connect", *_tenant_args(provider, tenant_id), "--json"], | ||
| cwd=cwd, | ||
| env=env, | ||
| input=key + "\n", | ||
| ) | ||
| if not isinstance(out, dict) or out.get("stored") != "local": | ||
| raise SwytchcodeError( | ||
| f"{provider} does not use an API key; use connect() instead", out | ||
| ) | ||
|
|
||
|
|
||
| def disconnect( | ||
| provider: str, | ||
| tenant_id: str, | ||
| *, | ||
| cwd: str | None = None, | ||
| env: dict[str, str] | None = None, | ||
| ) -> None: | ||
| """Remove one end user's account for a provider, on this machine and on Swytchcode.""" | ||
| run_cli( | ||
| ["auth", "disconnect", *_tenant_args(provider, tenant_id)], | ||
| cwd=cwd, | ||
| env=env, | ||
| json_output=False, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Replace the broken multi-tenant guide link.
The new guide link returns HTTP 404. Point readers to a published guide, or remove the link until the guide is available. ()
🤖 Prompt for AI Agents