> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth & secrets

> Token plumbing, split storage, and how the sandbox is kept token-free

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`](https://github.com/unifyai/unify/blob/main/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:

| Provider    | Aliases                                   | Secrets                                                                                                       |
| ----------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `google`    | `gmail`, `google_workspace`, `drive`      | `GOOGLE_ACCESS_TOKEN`, `GOOGLE_REFRESH_TOKEN`, `GOOGLE_TOKEN_EXPIRES_AT`, `GOOGLE_GRANTED_SCOPES`             |
| `microsoft` | `msft`, `ms365`, `microsoft_365`, `graph` | `MICROSOFT_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`](https://github.com/unifyai/unify/blob/main/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_policy` → `PolicyStore.set_policies` — see
[Provider proxy](/workspace/developers/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`](https://github.com/unifyai/unify/blob/main/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 exposure** —
  [`unify/function_manager/execution_env.py`](https://github.com/unifyai/unify/blob/main/unify/function_manager/execution_env.py)
  (`create_execution_globals`) places `get_oauth_access_token` — the
  nonce-returning one — into sandbox globals.
* **Environment scrubbing** — `provider_token_env_keys` in
  [`unify/provider_proxy/session.py`](https://github.com/unifyai/unify/blob/main/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`](https://github.com/unifyai/unify/blob/main/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`](https://github.com/unifyai/unify/blob/main/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).
