Skip to main content
This page covers the runtime half of the OAuth story: how a running assistant obtains, stores, and exposes workspace credentials — and how raw tokens are kept away from everything the LLM can read or execute.

The provider registry

unify/common/runtime_oauth.py is the single source of truth for what an “OAuth provider” is at runtime. OAuthProviderMetadata describes each provider — canonical name, aliases, and the secret names that hold its state — and _OAUTH_PROVIDER_METADATA registers the two built-ins:
ProviderAliasesSecrets
googlegmail, google_workspace, driveGOOGLE_ACCESS_TOKEN, GOOGLE_REFRESH_TOKEN, GOOGLE_TOKEN_EXPIRES_AT, GOOGLE_GRANTED_SCOPES
microsoftmsft, ms365, microsoft_365, graphMICROSOFT_ACCESS_TOKEN, MICROSOFT_REFRESH_TOKEN, MICROSOFT_TOKEN_EXPIRES_AT, MICROSOFT_GRANTED_SCOPES
_resolve_oauth_provider normalizes aliases, so actor code can say get_oauth_access_token("drive") and land on Google. Adding a new OAuth provider means adding a metadata entry here — the sync allowlist, proxy env, and prompt documentation all derive from it.

Two functions, one trust boundary

The module exposes two token accessors, and the difference between them is the security model:
  • get_provider_access_token(provider, min_ttl_seconds=300) — returns the real bearer token. Trusted-runtime only: used by WorkspaceEmailManager and by the proxy itself. If the token is missing or within the TTL window of expiry, it forces a secret sync from Orchestra first.
  • get_oauth_access_token(provider, min_ttl_seconds=300) — the sandbox-facing function. Returns a proxy nonce, never a real token. This is the symbol injected into execute_code globals.
Supporting pieces: get_provider_access_token_optimistic (no pre-expiry gate — the proxy uses it per-request and retries once on 401), refresh_provider_access_token (force a re-sync from Orchestra — note it does not call the provider’s token endpoint), and get_refresh_token_oauth_env_overlay (the env-var overlay for subprocesses). get_oauth_prompt_context renders the actor-facing documentation block for all of this.

Split storage in SecretManager

unify/secret_manager/secret_manager.py mirrors assistant secrets down from Orchestra (_sync_assistant_secrets hits the admin assistant endpoint and filters through _resolve_secret_allowlist, which unions the OAuth names from refresh_token_oauth_secret_names). The storage rule:
  • Sensitive — the raw access/refresh tokens (_sensitive_oauth_token_names) live only in the in-memory _oauth_tokens dict, read via get_oauth_token. They are never written to the Secrets context, .env, or os.environ.
  • Non-sensitive metadata*_TOKEN_EXPIRES_AT and *_GRANTED_SCOPES — is mirrored to the environment so that scope checks and expiry hints work everywhere, including inside the sandbox.
The same sync pass also pulls the workspace file-access policy (_sync_workspace_file_policyPolicyStore.set_policies — see Provider proxy). Sync triggers. sync_assistant_secrets_if_stale is a debounced gate called before every execute_code (from unify/actor/code_act_actor.py with a 60-second TTL), forced on assistant-update events (see unify/conversation_manager/domains/managers_utils.py), and forced by get_provider_access_token when a token is missing or near expiry. The practical upshot: reconnecting an account in the Console propagates to a live assistant within a minute without a restart. Refresh model. The runtime never performs OAuth refresh grants. A platform job refreshes tokens against Google/Microsoft and writes them to Orchestra; the runtime’s “refresh” is always pull the newer token down. This keeps refresh-token custody in exactly one place.

Keeping the sandbox token-free

Several mechanisms cooperate so that no raw token is observable from LLM-authored code:
  • Injection without exposureunify/function_manager/execution_env.py (create_execution_globals) places get_oauth_access_token — the nonce-returning one — into sandbox globals.
  • Environment scrubbingprovider_token_env_keys in unify/provider_proxy/session.py lists the sensitive keys, and build_sandbox_env strips them while overlaying the proxy variables (WORKSPACE_PROXY_URL, WORKSPACE_PROXY_TOKEN, MICROSOFT_GRAPH_BASE, GOOGLE_DRIVE_BASE, GOOGLE_API_BASE). Subprocess execution sites in unify/function_manager/function_manager.py and shell_session.py apply it; ensure_proxy_running additionally scrubs any lingering raw tokens from the parent os.environ.
  • Virtual-env isolation — functions running in dedicated venvs (unify/function_manager/venv_runner.py) get a get_oauth_access_token shim that RPCs the parent process (namespace runtime, handled by FunctionManager._handle_venv_rpc), so even a separate interpreter never receives the real token.
The result: the only way LLM-authored code can use a workspace credential is through the proxy, where policy is enforced.

Scope awareness

Granted scopes are deliberately visible (they’re metadata, not credentials). The actor prompt — _EXTERNAL_APP_INTEGRATION in unify/actor/prompt_builders.py — teaches the model a checking discipline: before calling an API, read GOOGLE_GRANTED_SCOPES / MICROSOFT_GRANTED_SCOPES (space-separated raw scope strings; Microsoft short names are prefixed with https://graph.microsoft.com/ except offline_access). If the secret is present but the needed scope is absent, don’t call the API — tell the user to reconnect with the extra feature from the Console. That behavioral rule is what turns a missing scope into a helpful message instead of a 403.

Tests

The behavior on this page is pinned by tests/common/test_runtime_oauth.py, tests/secret_manager/test_oauth_tokens.py, and tests/function_manager/test_runtime_oauth_bridge.py (the venv RPC bridge).