> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/openshell/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/openshell/_mcp/server.

# Profiles

> Use provider profiles to attach credentials, network policy, and refresh metadata to OpenShell sandboxes.

Provider profiles turn providers from credential records into profile-backed access bundles. A provider profile describes the credentials, endpoints, binaries, policy rules, and refresh behavior for a provider type. A provider instance stores the concrete credential and config values for one gateway.

Use provider profiles when you want provider-owned policy rules to travel with provider credentials. For example, a GitHub provider can describe both `GITHUB_TOKEN` and the GitHub API endpoints that a sandbox needs, so users do not have to copy the same network policy into every sandbox.

## Why Provider Profiles Exist

Provider credentials and network policy were previously configured through separate workflows. A user could create a GitHub provider that stored `GITHUB_TOKEN`, but the sandbox still needed a separate policy that allowed `api.github.com`, selected the right binaries, and configured REST enforcement.

Provider profiles keep those pieces together:

| Need                        | Profile-backed behavior                                                                                                                                                                            |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Repeatable provider setup   | Imported provider profiles define reusable provider types.                                                                                                                                         |
| Provider-aware policy       | Attached providers contribute `_provider_*` network policy entries to the effective sandbox policy.                                                                                                |
| Custom provider definitions | You can export, edit, lint, import, list, and delete custom profiles.                                                                                                                              |
| Runtime provider lifecycle  | You can list, attach, and detach providers on existing sandboxes.                                                                                                                                  |
| Credential rotation         | Provider refresh metadata lets the gateway refresh short-lived access tokens and update provider records.                                                                                          |
| Credential transport        | Credential delivery uses environment placeholders and proxy rewrite. Static placeholders resolve only at profile endpoints or explicitly bound sandbox policy endpoints for endpointless profiles. |

## Available Features

Provider profiles include these user-facing features:

* An import-only catalog: the gateway serves the profiles you imported and nothing else. A gateway with none imported serves an empty catalog.
* Gateway configuration can compose user-managed and interceptor-vended profile sources. Selecting only an interceptor makes its catalog authoritative by omission.
* Platform-scoped and workspace-scoped profiles. A workspace profile takes precedence over a platform profile with the same ID inside that workspace, and the platform profile applies outside it. An interceptor-vended profile cannot share an ID with an imported one; that collision fails closed.
* `openshell profile list` and `openshell profile describe` with table, YAML, and JSON output.
* `openshell profile export`, `import`, `update`, `lint`, and `delete` for custom profiles.
* Provider instances created from imported profile IDs with `openshell provider create --type <id>`.
* Provider instances whose submitted credentials can be stored by a configured gateway credential driver.
* Profile-backed credential discovery for explicit `openshell provider create --from-existing` and `openshell provider update --from-existing` flows. The `google-vertex-ai` profile also supplements discovery with Vertex config env vars such as `VERTEX_AI_PROJECT_ID` and `VERTEX_AI_REGION`.
* Just-in-time effective policy composition from sandbox policy plus attached provider profiles.
* Runtime sandbox provider commands under `openshell sandbox provider list|attach|detach|status`, with an option to wait until the sandbox applies a change.
* Credential refresh configuration with `openshell provider refresh status|configure|rotate|delete`.
* Credential expiry metadata with `openshell provider update --credential-expires-at`; values accept Unix epoch milliseconds or ISO/RFC3339 timestamps.
* Dynamic token grants that use the sandbox's SPIFFE JWT-SVID as an OAuth2 client assertion and inject short-lived tokens into supported headers for matching profile endpoints.
* Endpoint-bound static credential placeholders. The sandbox proxy resolves a static credential only for request hosts, ports, and paths declared by its provider profile or explicitly bound in sandbox policy for an endpointless profile.

## Understand Static Credential Endpoint Binding

Static credential endpoint binding prevents a placeholder for one service from
resolving on a different policy-allowed service. OpenShell associates every
static credential environment key with an endpoint boundary. Profile endpoints
supply that boundary by default. For an endpointless profile, a sandbox policy
endpoint can name the attached provider instance explicitly. The proxy checks
the resulting association before it substitutes the real value.

The example `openai` and `anthropic` profiles bind credentials to their public
vendor endpoints. To use a proxy or compatible API at another host, import a
separate profile that declares that host and attach a provider created from that
profile. OpenShell never broadens a profile's credential boundary from a base URL
environment variable alone: if a provider's `*_BASE_URL` points outside the hosts
its profile declares, the profile is treated as endpointless and its credential
binds only through explicit sandbox policy.

A request can use a static credential only when all of these checks pass:

| Check                                                                                     | Configuration source                                                                           |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| The placeholder belongs to the current attached provider state.                           | Sandbox provider attachment and current provider record.                                       |
| The calling binary and destination are allowed.                                           | Effective sandbox network policy.                                                              |
| The host, port, and canonical request path match the credential binding.                  | Provider profile endpoints, or an explicit sandbox policy binding for an endpointless profile. |
| The HTTP method, path, or protocol operation is allowed when L7 inspection is configured. | Effective sandbox network policy.                                                              |
| The credential has not expired.                                                           | Provider credential expiry metadata.                                                           |

Network policy and credential binding serve different purposes. Network policy
authorizes traffic. A credential binding authorizes use of one provider
instance's credentials at an admitted endpoint. A credential binding cannot
widen sandbox network policy.

For example, this profile endpoint binds all static credential environment keys
from the profile to `api.example.com:443` under `/v1`:

```yaml showLineNumbers={false}
endpoints:
  - host: api.example.com
    port: 443
    path: /v1/**
```

The path `/v1/**` matches `/v1` and its descendants. An empty path, `**`, or
`/**` matches every path on the selected host and port. Other path values use
glob matching. OpenShell removes the query string and uses a canonical,
secret-redacted path for this check, so a credential embedded in a request path
does not need to be revealed before authorization.

Use an explicit sandbox policy binding when the profile intentionally defines
credentials without defining service endpoints. The policy names the concrete
provider instance, not the profile type:

```yaml showLineNumbers={false}
network_policies:
  gcp_storage:
    endpoints:
      - host: storage.googleapis.com
        port: 443
        protocol: rest
        access: full
        credential_binding:
          provider: work-gcp
```

The provider must be attached to the sandbox and must select an endpointless
profile. OpenShell rejects the complete policy update if the provider is
unattached, has no profile, or selects a profile that already defines endpoints.
This keeps one source of credential-binding authority for each provider.
`credential_binding` is sandbox-scoped and is not accepted in a gateway-global
policy.

For AWS endpoints, the binding and signing fields have separate jobs.
`credential_binding.provider` selects the provider instance that supplies
credentials. `credential_signing`, `signing_service`, and `signing_region`
control how the proxy applies those credentials:

```yaml showLineNumbers={false}
network_policies:
  aws_s3:
    endpoints:
      - host: s3.us-west-2.amazonaws.com
        port: 443
        protocol: rest
        access: full
        credential_binding:
          provider: work-aws
        credential_signing: sigv4
        signing_service: s3
        signing_region: us-west-2
```

The binding applies to CONNECT and forward-proxy HTTP requests, including
headers, Basic and Bearer authorization, URL paths, query parameters, opted-in
request bodies, AWS SigV4 signing, and opted-in WebSocket text messages. Raw
`tls: skip` and non-HTTP tunnels do not perform static credential substitution.

Before it activates a sandbox policy, OpenShell verifies that every endpoint
with `credential_signing` has an attached profile that declares
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. An endpoint-bearing profile must
cover the signed host, port, and path. An endpointless profile must be selected
by `credential_binding.provider` on that endpoint. A missing or mismatched
source rejects the whole policy update with `FAILED_PRECONDITION`.

If an HTTP request contains a known placeholder at a destination outside its
binding, OpenShell returns HTTP 403 with this response:

```json
{"error":"credential_endpoint_mismatch","message":"Credential is not authorized for this request endpoint"}
```

The sandbox logs include the destination and
`credential_endpoint_mismatch`. OCSF output includes both the denied activity
and a detection finding. These events omit credential values, placeholders,
environment keys, and query strings.

For opted-in WebSocket text-message rewriting, the mismatch can occur after the
HTTP 101 upgrade has completed. OpenShell closes that WebSocket with policy
violation code 1008 instead of returning an HTTP response.

Binding updates apply to both current and retained placeholder generations.
For gateway-managed refresh credentials, automatic and manual token rotation
keep one opaque workload placeholder and replace only its current resolver
value. This lets a long-running process use each newly minted access token
without restarting. OpenShell does not retain older values behind this stable
placeholder.

Explicitly configuring refresh again starts a new authorization epoch, even
when the provider and credential key are unchanged. Reconfiguration, provider
replacement or detachment, refresh deletion, and endpoint-boundary changes
revoke the old placeholder. Updating a provider profile changes the binding on
the next sandbox provider-environment sync.

> **Warning**
>
> Static credentials require at least one usable binding. OpenShell withholds only
> the static credential keys and associated expiry and binding metadata from an
> endpointless selected profile when no sandbox policy endpoint explicitly binds
> that provider. It retains that provider's generated non-secret configuration
> and valid endpoint-bound static credentials from other attached providers.

After upgrading a gateway and supervisor to a release with endpoint binding,
restart or recreate older running sandboxes. A new gateway withholds static
credential material from supervisors that do not advertise binding support.
Rotate attached static credentials after upgrading when an older sandbox may
have received their real values.

When upgrading from revision-scoped refresh placeholders to stable refresh
handles, restart each existing workload once so it receives the new placeholder.
Later access-token rotations do not require workload restarts.

## Roadmap

The following provider profile design items are not part of the current behavior:

| Roadmap item                                | Current behavior                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| General profile-driven credential placement | Static `auth_style`, `header_name`, `query_param`, and `path_template` placement metadata is stored and validated, but static credential injection still depends on environment placeholders generated from provider credentials. Dynamic `token_grant` credentials support `bearer` and `header` placement for matching HTTP endpoints. |
| Binary-scoped credential injection          | Provider profile binaries affect policy composition but do not yet restrict placeholder resolution by calling binary. Static and dynamic credentials are endpoint-scoped.                                                                                                                                                                |
| Credential verification on create           | `openshell provider create` does not yet probe provider verification endpoints or expose `--no-verify`.                                                                                                                                                                                                                                  |
| Automatic credential scope extraction       | OpenShell does not yet inspect upstream provider responses to discover credential scopes.                                                                                                                                                                                                                                                |
| Policy prover integration                   | OpenShell does not yet run the policy prover automatically on sandbox startup or block startup based on prover findings.                                                                                                                                                                                                                 |
| Refresh telemetry as OCSF events            | Credential refresh logs are secret-safe gateway logs. OCSF refresh events and metrics are future work.                                                                                                                                                                                                                                   |

Use [Inference](/sandboxes/inference-routing) to attach an
inference provider and call its native endpoint.

## Provider Profiles

A provider profile defines a provider type. It contains metadata, credential declarations, endpoint policy, binary policy, an informational provider category, and optional credential refresh metadata.

Use `openshell profile` to discover, inspect, and manage reusable profile definitions. Use `openshell provider` to manage provider instances and their credential values. Provider profiles are the supported profile type.

Existing command names remain supported. `openshell provider list-profiles` is equivalent to `openshell profile list --type provider`, and `openshell provider profile export/import/update/lint/delete` invokes the same handlers as the corresponding `openshell profile` commands. The nested commands retain their existing arguments, output options, and workspace/global flags. New examples use the top-level form.

List available profiles:

```shell
openshell profile list
openshell profile list --type provider -o json
```

The default table shows `NAME`, `TYPE`, `CATEGORY`, `SOURCE`, and `SCOPE`. `NAME` is the profile ID used by the other profile commands and by `provider create --type`. The optional `--type provider` filter selects provider profiles. Use `-o json` or `-o yaml` for structured output.

Inspect one profile before creating a provider:

```shell
openshell profile describe github
openshell profile describe github -o yaml
```

The description shows metadata, credential names and authentication settings, endpoints with their protocols and policy rule counts, allowed binaries, source, and scope. Endpoint details include TLS handling, permission for uninspected credential traffic, and MCP method and tool-name settings. A `tls: skip` endpoint is shown as a raw tunnel without L7 inspection or credential rewrite, even when it declares an L7 protocol and rules. Use JSON or YAML output to inspect the complete rule definitions. The description reads the reusable definition without reading credential values from provider instances. A missing profile ID returns an error.

List and describe use the selected workspace's effective catalog. Pass the inherited `--workspace` flag to select another workspace, or `--global` to target platform scope. Platform operations require Platform Admin access.

```shell
openshell profile list --workspace team-ml
openshell profile describe github --workspace team-ml
openshell profile list --global
openshell profile describe github --global
```

A gateway serves exactly the profiles you imported. OpenShell ships no profiles inside the gateway binary, so a new gateway lists an empty catalog until you import one. When a configured gateway interceptor vends an authoritative provider profile catalog, that catalog becomes the visible source of truth: list, describe, export, provider creation, policy composition, and sandbox provider environment resolution use the interceptor-vended profiles.

### Import the Example Profiles

The OpenShell repository ships example profiles for GitHub, PyPI, and the major
inference and agent providers in
[`providers/`](https://github.com/NVIDIA/OpenShell/tree/main/providers). They are
reviewable starting points, not platform defaults.

Read a file's header before importing it. Each one names the client binaries it
expects, the reference image layout those paths assume, the credential scope,
the endpoint access it grants, and a smoke test. Several name layout-specific
paths, such as `/sandbox/.venv` or `/usr/lib/node_modules/@openai`. Imported
unchanged into a different image the
profile matches nothing: the catalog still lists it, but the credential is never
injected and the traffic is denied. Copy the file, edit `binaries` and
`endpoints` for your image and workload, and import your copy.

Lint a profile before importing it:

```shell
openshell profile lint -f providers/github.yaml
```

Import one profile file at platform scope:

```shell
openshell profile import -f providers/github.yaml --global
```

Import all non-recursive `*.yaml`, `*.yml`, and `*.json` files from a directory:

```shell
openshell profile import --from ./providers --global
```

Omit `--global` to import into the current workspace instead.

Export a profile as YAML, to edit or to keep before an upgrade:

```shell
openshell profile export github -o yaml --global > github-profile.yaml
```

Import is create-only. It fails if a custom profile with the same ID already exists.

Update an existing custom profile by exporting the current custom profile, editing the file, and submitting the edited file back:

```shell
openshell profile export github-profile -o yaml > github-profile.yaml
openshell profile update github-profile -f github-profile.yaml
```

Exported custom profiles include `resource_version`. OpenShell requires that version during update so stale files cannot silently overwrite newer profile definitions. The target ID in the command must match the profile ID in the file. Update accepts one file at a time. If an update would make dynamic token grants ambiguous for an attached sandbox, OpenShell rejects it before changing the profile.

Delete custom profiles by ID:

```shell
openshell profile delete custom-api custom-alt
```

Export accepts `-o yaml` or `-o json` and defaults to YAML. Lint also accepts `--from <directory>` and asks the gateway to validate the supplied definitions without storing them. Export, import, update, lint, and delete use the selected workspace unless you pass `--global`.

Profile IDs must use lowercase kebab-case with `a-z`, `0-9`, and `-`. No IDs are reserved: `github`, `openai`, and every other example profile imports at its own ID, and the imported profile is the only definition for it. Interceptor-managed profiles are read-only through the profile APIs. OpenShell rejects deleting a profile while a sandbox-attached provider uses it.

### Category Enum

The `category` field supplies the `CATEGORY` column in `openshell profile list`. Use one of these canonical YAML values:

| Value            | Use for                                                                              |
| ---------------- | ------------------------------------------------------------------------------------ |
| `other`          | Profiles that do not fit a more specific category. This is the default when omitted. |
| `inference`      | Model and inference API providers.                                                   |
| `agent`          | Agent CLIs and coding tools.                                                         |
| `source_control` | Git hosting, repository, and source control providers.                               |
| `messaging`      | Chat, email, notification, and messaging APIs.                                       |
| `data`           | Data storage, file, database, and document APIs.                                     |
| `knowledge`      | Search, retrieval, and knowledge-base providers.                                     |

### Profile Schema

Provider profile YAML and JSON use this shape. Treat this as a field map, not a profile to import verbatim. The endpoint and rule fields mirror the network policy schema used under `network_policies`. Refer to [Policy Schema Reference](/reference/policy-schema) for field semantics.
Use `annotations` only for non-secret metadata such as source, signature, or governance markers. OpenShell preserves annotations through profile import, export, and interceptor-managed profile snapshots.

```yaml wordWrap showLineNumbers={false}
id: custom-api
# Present on exported custom profiles; preserve it when updating.
resource_version: 1
annotations:
  example.com/source: platform
display_name: Custom API
description: Custom API access for sandbox agents
category: data
inference_capable: false

credentials:
  - name: user_oidc_token
    description: User OIDC token stored for gateway-side token exchange only
    required: true

  - name: api_token
    description: API access token
    env_vars: [CUSTOM_API_TOKEN]
    required: true

    # Accepted values: basic, bearer, header, query, path.
    # These fields describe static credential placement.
    # Static runtime injection still uses env placeholder resolution.
    auth_style: bearer
    header_name: authorization
    query_param: api_key
    path_template: /v1/{credential}/resources

    refresh:
      # Accepted values:
      # static, external, oauth2_refresh_token,
      # oauth2_client_credentials, google_service_account_jwt.
      strategy: oauth2_client_credentials
      token_url: https://login.example.com/oauth2/token
      scopes: [api.read, api.write]
      refresh_before: "300s"
      max_lifetime: "3600s"
      material:
        - name: client_id
          description: OAuth client ID
          required: true
          secret: false
        - name: client_secret
          description: OAuth client secret
          required: true
          secret: true

    # Optional dynamic credential. The sandbox supervisor resolves this on
    # demand for matching endpoint traffic, caches the returned access token,
    # and injects it according to auth_style/header_name.
    token_grant:
      # Accepted values: client_credentials, token_exchange.
      grant_type: token_exchange
      token_endpoint: https://login.example.com/realms/custom/protocol/openid-connect/token
      audience: api://custom-api
      jwt_svid_audience: https://login.example.com/realms/custom
      client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-spiffe
      scopes: [api.read, api.write]
      cache_ttl: "300s"
      requested_token_type: urn:ietf:params:oauth:token-type:access_token
      subject_token:
        source: provider_credential
        credential: user_oidc_token
        subject_token_type: urn:ietf:params:oauth:token-type:access_token
      audience_overrides:
        - host: api.example.com
          port: 443
          path: /v1/projects/**
          audience: api://custom-projects
          scopes: [projects.read]

discovery:
  credentials: [api_token]

endpoints:
  - host: api.example.com
    port: 443
    path: /v1/**
    protocol: rest
    tls: ""
    enforcement: enforce
    allowed_ips: []
    ports: []
    allow_encoded_slash: false
    websocket_credential_rewrite: false
    request_body_credential_rewrite: false
    allow_uninspected_credentials: false
    persisted_queries: deny
    graphql_max_body_bytes: 65536
    rules:
      - allow:
          method: GET
          path: /v1/projects/**
          command: ""
          query:
            tag:
              any: ["prod-*", "staging-*"]
          operation_type: ""
          operation_name: ""
          fields: []
    deny_rules:
      - method: DELETE
        path: /v1/projects/**
        command: ""
        query: {}
        operation_type: ""
        operation_name: ""
        fields: []
    graphql_persisted_queries:
      # Key must match the request's persisted query hash or saved-query ID.
      9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08:
        operation_type: query
        operation_name: GetProject
        fields: [project]

binaries:
  - /usr/bin/curl
  - /usr/local/bin/custom-cli
```

Duration fields use protobuf duration strings and preserve fractional seconds.
An absent `refresh_before` uses the gateway default, while `"0s"` refreshes at
expiry. An absent `max_lifetime` uses the strategy default; an explicit zero
maximum lifetime is invalid. Existing `*_seconds` fields remain accepted on
import for compatibility.

### Profile Sections

`id`, `display_name`, and `description` identify the profile. `id` is the value passed to `openshell provider create --type`.

`category` supplies the `CATEGORY` column in `openshell profile list`. Use one of the values in the category enum.

`credentials` declares the credential names, environment variables, auth metadata, optional refresh metadata, and optional dynamic token grant metadata for the provider type. The `auth_style` field accepts `basic`, `bearer`, `header`, `query`, or `path`. When `auth_style` is `path`, set `path_template` to a URL path containing the `{credential}` placeholder exactly once (for example, `/v1/{credential}/resources`). Static credentials are exposed as placeholder environment variables and resolved in outbound HTTP requests only at their binding endpoints. Every static credential environment key receives the full profile endpoint set when the profile defines endpoints. An endpointless profile requires explicit sandbox policy bindings for each attached provider instance. Dynamic token grants are resolved by the sandbox proxy on demand for matching profile endpoints and support `bearer` or `header` placement. Credential environment variable names must not use the reserved `v<digits>_` prefix, such as `v10_GITHUB_TOKEN`, because OpenShell uses that namespace for revision-scoped placeholders.

`discovery` controls what `--from-existing` scans. Each entry in
`discovery.credentials` must name a
credential declared under `credentials`. OpenShell scans the referenced
credential's `env_vars` in order and stores the first non-empty local
environment value under the actual environment variable key.

`endpoints` contains the same endpoint object shape as sandbox network policy. A profile can use access presets, protocol-specific allow rules, deny rules, WebSocket credential rewriting, request body credential rewriting, GraphQL fields, and SSRF IP allowlists. Because profile credentials are not mapped to individual endpoints, OpenShell conservatively treats every endpoint in a profile that declares credentials as credentialed. Such endpoints require L7 inspection and cannot use `tls: skip` unless the profile explicitly sets `allow_uninspected_credentials: true`.

Profile validation rejects unknown `tls`, `enforcement`, and `access` values before the profile can contribute policy to a sandbox.

`binaries` contains the executable paths allowed to reach the profile endpoints when the profile contributes policy to a sandbox. Write each binary as a scalar path. OpenShell also accepts the transitional object form `- path: /usr/bin/example` and exports it as a scalar. The removed `harness` property is rejected; delete it from profiles created before 0.1.0.

`inference_capable` is informational metadata that marks profiles intended for
model and inference APIs. It does not grant access, select a model, configure a
client, or change routing. The sandbox must attach a provider instance, and the
workload calls the profile-authorized native endpoint.

### Refresh Metadata

Credential refresh metadata belongs to one credential declaration. The profile defines allowed defaults, such as token URL, scopes, refresh lead time, maximum lifetime, and required material keys. The provider instance stores the actual refresh material.

Profile YAML can declare these refresh strategies:

| Strategy                     | Behavior                                                                                                                             |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `static`                     | Current credentials are updated through `openshell provider update`. The gateway does not mint a token.                              |
| `external`                   | An external process updates current credentials through `openshell provider update`. The gateway does not mint a token.              |
| `oauth2_refresh_token`       | The gateway exchanges a refresh token for a short-lived access token.                                                                |
| `oauth2_client_credentials`  | The gateway mints a short-lived access token with OAuth2 client credentials.                                                         |
| `google_service_account_jwt` | The gateway signs a Google service account JWT and exchanges it for an access token.                                                 |
| `aws_sts_assume_role`        | The gateway calls `sts:AssumeRole` and mints `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` in one operation. |

`openshell provider refresh configure` accepts only gateway-mintable strategies: `oauth2-refresh-token`, `oauth2-client-credentials`, `google-service-account-jwt`, and `aws-sts-assume-role`. Use `openshell provider update` for `static` and `external` refresh patterns.

Gateway-managed refresh strategies use these material keys:

| Strategy                     | Material keys                                                                                                                                                                         |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `oauth2_refresh_token`       | `client_id`, `refresh_token`, optional `client_secret`.                                                                                                                               |
| `oauth2_client_credentials`  | `client_id`, `client_secret`, optional `tenant_id` for Microsoft Entra token URLs.                                                                                                    |
| `google_service_account_jwt` | `client_email`, `private_key`, optional `subject` or `sub`.                                                                                                                           |
| `aws_sts_assume_role`        | `role_arn`, optional `session_name`, `external_id`, `aws_region`, and optional long-lived `aws_access_key_id` / `aws_secret_access_key` for gateways without ambient AWS credentials. |

OpenShell keeps token endpoints profile-owned. Refresh material cannot override `token_url` or `token_uri` during refresh configuration.

### Additional Outputs

Most refresh strategies mint a single credential. `aws_sts_assume_role` mints three. A refresh declares the extra credentials it co-mints with `additional_outputs`, mapping each strategy-defined output id to a sibling credential whose `env_vars` receive the value:

```yaml
credentials:
  - name: access_key_id
    env_vars: [AWS_ACCESS_KEY_ID]
    required: true
    refresh:
      strategy: aws_sts_assume_role
      additional_outputs:
        - output: secret_access_key
          credential: secret_access_key
        - output: session_token
          credential: session_token
  - name: secret_access_key
    env_vars: [AWS_SECRET_ACCESS_KEY]
    required: true
  - name: session_token
    env_vars: [AWS_SESSION_TOKEN]
    required: true
```

The refresh attaches to the primary credential (`access_key_id`). Each referenced sibling must exist, declare exactly one env var, and not carry its own refresh. Because a refresh's outputs are runtime-resolvable, a profile whose only credentials are STS-minted can be created with `--runtime-credentials`. The resolved output-to-env-key mapping is pinned when refresh is configured, so later profile edits do not silently redirect where minted values are written.

### Dynamic Token Grants

`token_grant` belongs to one credential declaration. When a sandbox with the provider attached sends HTTP traffic to a matching profile endpoint, the supervisor resolves the dynamic credential, caches the returned access token, and injects it before forwarding the request upstream. Use `auth_style: bearer` to inject `Authorization: Bearer <token>`, or `auth_style: header` with `header_name` to inject the raw access token into a custom header. Token grants do not support `query` or `path` placement.

OpenShell supports two dynamic grant types:

| Grant type           | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_credentials` | The supervisor requests a SPIFFE JWT-SVID from the local Workload API and sends it directly to `token_endpoint` as the OAuth2 client assertion. This is the default when `grant_type` is omitted.                                                                                                                                                                                                                                                                       |
| `token_exchange`     | The supervisor first asks the gateway for an intermediate token. The request includes the supervisor JWT-SVID; the gateway verifies it, uses the SVID subject as the intermediate token audience, and exchanges the stored subject credential at the same `token_endpoint` using the gateway's own JWT-SVID as the client assertion. The supervisor then exchanges that intermediate token for the final upstream token using its own JWT-SVID as the client assertion. |

Create provider instances for token-grant-only profiles with `--runtime-credentials`. This records an empty provider instance and makes the runtime-resolved credential source explicit:

```shell
openshell provider create \
  --name spiffe-token-demo \
  --type spiffe-token-demo \
  --runtime-credentials
```

For `token_exchange` profiles, the provider also stores the user subject token referenced by `token_grant.subject_token.credential`. That credential is gateway-only: the sandbox does not receive it as environment material, static credential binding metadata, or workload placeholder ownership. Create or update that provider credential from the current gateway OIDC login with `--from-oidc-token`. This requires an active named gateway that was registered for OIDC. The CLI copies the current OIDC access token and its expiry into the provider. If the stored gateway access token is expired and a refresh token is available, the CLI refreshes it first. OpenShell does not store the OIDC refresh token in the provider. When the stored subject-token credential expires, the gateway rejects intermediate token exchange until the provider is updated with a fresh token.

```shell
openshell provider create \
  --name custom-api \
  --type custom-api \
  --from-oidc-token

openshell provider update custom-api \
  --from-oidc-token
```

OpenShell infers the destination credential when the provider profile has exactly one `token_grant.subject_token.credential`. If a profile declares more than one token-exchange subject credential, pass `--credential <key>` to choose one.

Token grant fields:

| Field                              | Required                      | Behavior                                                                                                                                                                                                                                                                                                                                                             |
| ---------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grant_type`                       | No                            | `client_credentials` or `token_exchange`. Defaults to `client_credentials` for backward compatibility.                                                                                                                                                                                                                                                               |
| `token_endpoint`                   | Yes                           | OAuth2 token endpoint that accepts a SPIFFE JWT-SVID client assertion. Use `https://` unless the endpoint is loopback or a Kubernetes service DNS name such as `token-issuer.default.svc.cluster.local`.                                                                                                                                                             |
| `audience`                         | No                            | Resource audience requested from the token service. For `token_exchange`, this is the final exchange audience; the gateway intermediate exchange always uses the verified supervisor SVID subject as its audience.                                                                                                                                                   |
| `jwt_svid_audience`                | No                            | Audience used when requesting the JWT-SVID. When omitted, OpenShell derives an issuer-style audience from Keycloak token endpoint paths or falls back to the full token endpoint URL.                                                                                                                                                                                |
| `client_assertion_type`            | No                            | OAuth2 `client_assertion_type` form value. Defaults to RFC 7523 `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. Set `urn:ietf:params:oauth:client-assertion-type:jwt-spiffe` when the token issuer expects the SPIFFE assertion type.                                                                                                                      |
| `scopes`                           | No                            | OAuth2 scopes sent as a space-separated `scope` parameter.                                                                                                                                                                                                                                                                                                           |
| `cache_ttl`                        | No                            | Protobuf-duration cache override, such as `300s` or `0.500s`. Omission uses the token response expiry; `0s` disables caching. The legacy `cache_ttl_seconds` input remains accepted.                                                                                                                                                                                 |
| `requested_token_type`             | No                            | RFC 8693 `requested_token_type` sent during token exchange. Defaults to `urn:ietf:params:oauth:token-type:access_token`.                                                                                                                                                                                                                                             |
| `subject_token`                    | Required for `token_exchange` | Subject-token source used for the gateway-brokered intermediate exchange. Phase one supports `source: provider_credential`, where `credential` names another credential declared in the same profile. That referenced credential is broker-only and cannot declare workload injection metadata such as env vars, static placement, refresh, or token grant metadata. |
| `subject_token.subject_token_type` | No                            | RFC 8693 `subject_token_type` for the stored subject token. Defaults to `urn:ietf:params:oauth:token-type:access_token`.                                                                                                                                                                                                                                             |
| `audience_overrides`               | No                            | Endpoint-specific final-exchange `audience` and `scopes` overrides selected by host, port, and path. These overrides do not affect the gateway intermediate exchange.                                                                                                                                                                                                |

Token grants require the sandbox supervisor to have access to a SPIFFE Workload API socket. `token_exchange` also requires the gateway to have its own Workload API socket so it can present a gateway JWT-SVID during the intermediate exchange. They apply to HTTP traffic that the proxy can inspect. Endpoints with `tls: skip` bypass TLS termination and cannot receive dynamic token grant injection for HTTPS traffic. The token service must return a token value that is safe for HTTP header placement; malformed values are rejected before caching or header injection.

The gateway only brokers an intermediate token for a sandbox principal, and only when the requested provider is attached to that sandbox. It verifies the supervisor JWT-SVID issuer, audience, signature, and SPIFFE trust domain against the gateway's own JWT-SVID, then uses the verified supervisor SVID subject as the intermediate-token audience.

## Provider Instances

A provider instance stores concrete credentials and config for a profile type. `--type` accepts the ID of any imported profile, matched exactly. Creating a provider for a profile the gateway does not serve fails and names the import command.

Create a GitHub provider from an imported `github` profile:

```shell
openshell provider create \
  --name work-github \
  --type github \
  --credential GITHUB_TOKEN
```

Create a provider from local credentials discovered through the provider profile:

```shell
openshell provider create \
  --name work-claude \
  --type claude-code \
  --from-existing
```

`--from-existing` uses the provider profile's `discovery` section. If no
profile exists for the requested type, the command fails instead of falling
back to the retired provider registry. The example `openai` profile discovers
`OPENAI_API_KEY`; the example `anthropic` profile discovers
`ANTHROPIC_API_KEY`.

Create a provider from another imported profile:

```shell
openshell provider create \
  --name custom-api \
  --type custom-api \
  --credential CUSTOM_API_TOKEN
```

Create a provider whose credential is stored by a configured gateway credential
driver:

```shell
openshell provider create \
  --name openai-stored \
  --type openai \
  --credential OPENAI_API_KEY
```

The create/update API stores submitted provider credentials and secret refresh
material through the gateway's active credential storage path and persists only
internal credential handles. Secret refresh material includes OAuth refresh
tokens, client secrets, service-account private keys, and temporary AWS source
secrets. By default, the gateway stores AES-256-GCM encrypted credential
envelopes in the gateway database outside provider and refresh-state records. The Helm chart
creates a retained Kubernetes Secret for the default storage key-encryption key
and injects it into every gateway pod when no external credential driver is enabled.
`credential_drivers = []` is invalid. Multi-replica Kubernetes gateways can use
a shared database with the default encrypted store, or choose a shared backend
such as `kubernetes-secrets` or `vault`.

Every `secret_material_keys` entry must name a key supplied in `material` in the
same configure request. Clients submit secret values, not internal credential
handles.

Provider records that already contain inline database credentials remain
readable for upgrade compatibility. New provider create/update requests store
credential values through the active credential driver and persist only handles.

Provider profiles whose required credentials are fully runtime-resolvable through `token_grant` or gateway-managed refresh can be created without `--credential`.

Inspect the provider:

```shell
openshell provider get custom-api
```

Update provider credentials:

```shell
openshell provider update custom-api --credential CUSTOM_API_TOKEN
```

Set or clear credential expiry metadata:

```shell
openshell provider update custom-api \
  --credential CUSTOM_API_TOKEN="$CUSTOM_API_TOKEN" \
  --credential-expires-at CUSTOM_API_TOKEN=2026-01-01T00:00:00Z
```

Use an ISO/RFC3339 timestamp or Unix epoch milliseconds. Use `0` as the timestamp to clear expiry for a credential key.

OpenShell skips expired provider credentials when it builds a sandbox provider environment. Running sandboxes also reject expired retained credential generations during placeholder resolution, so stale placeholders fail closed instead of forwarding unresolved or expired credential material.

The gateway sends a complete host, port, and path binding for every emitted static credential key. It derives bindings from profile endpoints or explicit sandbox policy endpoints for endpointless profiles. It withholds static credential keys from endpointless selected profiles that have no explicit policy binding. Supervisors reject other incomplete binding metadata and clear previously active provider material when a refresh fails validation. Refer to [Static Credential Endpoint Binding](#understand-static-credential-endpoint-binding) for matching, denial, lifecycle, and migration behavior.

## Configure Credential Refresh

Refresh configuration is stored separately from the current injectable credential value. Non-secret refresh configuration remains in the refresh-state record. Secret material is resolved from the active credential driver only while the gateway mints a new short-lived token. The gateway writes the token back through credential storage and updates credential expiry metadata. If an OAuth issuer rotates its refresh token, the gateway stages the replacement through credential storage before committing the refresh generation and removes the previous handle afterward.

For OAuth token endpoint failures, the gateway parses the bounded standard OAuth
error response and reports a structured recovery action through `provider
refresh status`. It does not retain provider error descriptions or raw response
bodies. The recovery actions are:

* `retry`: a network, rate-limit, or temporary issuer failure; the worker retries automatically.
* `reauthorize`: a user refresh grant is no longer usable, such as `invalid_grant`; obtain a new grant and run `provider refresh configure`.
* `fix_configuration`: the OAuth client, scopes, grant type, or administrator policy must be corrected.
* `investigate`: the issuer returned an unrecognized response; the worker retries, but the operator should investigate persistent failures.

The gateway parks `reauthorize` and `fix_configuration` failures instead of
retrying them every worker tick. A manual `provider refresh rotate` still
attempts the exchange. The current access token remains available only until
its recorded expiry, after which credential resolution fails closed.

Before OpenShell 0.1.0, refresh-state and credential-driver migrations are not
supported. Upgrading from a build that stored refresh material inline requires
reconfiguring the refresh grant. To change credential drivers, delete or
reconfigure affected providers while the original driver is still available,
then select the new driver and create the credentials again. Do not run mixed
gateway versions against the same refresh records.

Each explicit `refresh configure` call also starts a new gateway-owned
authorization epoch. Automatic refresh and `refresh rotate` preserve that epoch,
so running workloads keep the same opaque credential handle while the short-lived
token changes. Configuring refresh again is a revocation boundary and causes
running workloads that still hold the previous handle to fail closed.

While gateway-managed refresh is configured, `provider update --credential`
cannot replace or delete its primary credential or any co-minted output. Use
`provider refresh rotate` to mint a new short-lived value. Re-run
`provider refresh configure` to start a new authorization, or use
`provider refresh delete` before returning those credential keys to manual
management. You can still update unrelated credentials, configuration, and
credential expiry metadata.

For a complete Microsoft Graph OAuth2 refresh-token walkthrough, see [Refresh Microsoft Graph Credentials with a Provider Profile](/get-started/tutorials/microsoft-graph-provider-refresh).

The profile YAML strategy values use underscores, while the CLI `--strategy` values use kebab-case:

| Profile YAML                 | CLI                          |
| ---------------------------- | ---------------------------- |
| `oauth2_refresh_token`       | `oauth2-refresh-token`       |
| `oauth2_client_credentials`  | `oauth2-client-credentials`  |
| `google_service_account_jwt` | `google-service-account-jwt` |

Create the provider instance first:

```shell
openshell provider create \
  --name my-graph \
  --type microsoft-graph-mail \
  --credential MS_GRAPH_ACCESS_TOKEN
```

This example assumes you imported a custom profile with
`id: microsoft-graph-mail`. Provider refresh can be configured only for provider
types whose profile declares compatible credential refresh metadata.

Configure OAuth2 client credentials refresh:

```shell
openshell provider refresh configure my-graph \
  --credential-key MS_GRAPH_ACCESS_TOKEN \
  --strategy oauth2-client-credentials \
  --material tenant_id="$MS_TENANT_ID" \
  --material client_id="$MS_CLIENT_ID" \
  --material client_secret="$MS_CLIENT_SECRET" \
  --secret-material-key client_secret \
  --credential-expires-at 2026-01-01T00:00:00Z
```

Configure OAuth2 refresh-token refresh:

```shell
openshell provider refresh configure my-graph \
  --credential-key MS_GRAPH_ACCESS_TOKEN \
  --strategy oauth2-refresh-token \
  --material client_id="$MS_CLIENT_ID" \
  --material refresh_token="$MS_REFRESH_TOKEN" \
  --material client_secret="$MS_CLIENT_SECRET" \
  --secret-material-key refresh_token \
  --secret-material-key client_secret
```

Configure Google service account JWT refresh:

```shell
openshell provider create \
  --name drive-work \
  --type google-drive \
  --credential GOOGLE_DRIVE_ACCESS_TOKEN

openshell provider refresh configure drive-work \
  --credential-key GOOGLE_DRIVE_ACCESS_TOKEN \
  --strategy google-service-account-jwt \
  --material client_email="$GOOGLE_CLIENT_EMAIL" \
  --material private_key="$GOOGLE_PRIVATE_KEY" \
  --secret-material-key private_key
```

This example assumes you imported a custom profile with `id: google-drive`.

> **Note**
>
> `--secret-material-key` takes the name of a `--material` key, not the secret value. For example, use `--material client_secret="$MS_CLIENT_SECRET"` with `--secret-material-key client_secret`. Prefer `--secret-material-env` so the value does not appear in shell history. The gateway combines caller markings with authoritative profile metadata and strategy-defined secret fields, then stores those values through the active credential driver. Only the `--credential-key` value, such as `MS_GRAPH_ACCESS_TOKEN`, becomes injectable. If an OAuth response rotates a refresh token, OpenShell stores the replacement through the credential driver automatically.

Use `--credential-expires-at` when the current provider credential already has a known expiry timestamp. For refresh-managed keys, the value can be Unix epoch milliseconds or an ISO/RFC3339 timestamp such as `2026-01-01T00:00:00Z` or `2026-01-01T01:00:00+01:00`. OpenShell stores that value as epoch milliseconds in both refresh state and provider credential metadata. An explicit Unix epoch value remains distinct from an omitted expiry and is already expired. Later gateway-managed refreshes replace it with the minted token expiry.

Force a refresh immediately:

```shell
openshell provider refresh rotate my-graph \
  --credential-key MS_GRAPH_ACCESS_TOKEN
```

Check refresh status:

```shell
openshell provider refresh status my-graph
```

The status table includes `RECOVERY` and `FAILURE_CODE`. Clients should use the
structured recovery action rather than parsing `LAST_ERROR`. For example,
`oauth_invalid_grant` with recovery action `reauthorize` means the user must
complete OAuth authorization again; `oauth_invalid_client` with
`fix_configuration` means changing the user login alone will not repair the
grant. OpenShell parks `reauthorize` failures until an explicit rotation or
reconfiguration. It retries `fix_configuration` failures hourly so externally
repaired clocks, policies, or credential-store values can recover without
creating rapid token-endpoint traffic. Transient and unrecognized failures use
the existing bounded 60-second retry interval.

The status table reports operational state without printing token values or refresh material:

```text
PROVIDER                  CREDENTIAL_KEY                STRATEGY                      STATUS                    RECOVERY            EXPIRES_AT            NEXT_REFRESH          LAST_REFRESH          FAILURE_CODE                                  LAST_ERROR
my-graph                  MS_GRAPH_ACCESS_TOKEN         oauth2_refresh_token          reauthorization_required  reauthorize         2026-06-01 00:00:00   -                     2026-05-31 23:00:00   oauth_invalid_grant                           OAuth refresh grant is no longer usable
```

When no refresh configuration exists, the CLI distinguishes whole-provider checks from credential-specific checks:

```text
No refresh configurations found for provider 'my-graph'.
No refresh configuration found for provider 'my-graph' credential 'MS_GRAPH_ACCESS_TOKEN'.
```

Delete refresh state for one credential:

```shell
openshell provider refresh delete my-graph \
  --credential-key MS_GRAPH_ACCESS_TOKEN
```

Deleting refresh state clears the provider credential expiry only when that expiry came from the deleted refresh state, including an explicit Unix epoch expiry. If you later set a different expiry manually with `openshell provider update --credential-expires-at`, OpenShell preserves the manual value.

### Refresh Logs

The gateway emits secret-safe refresh logs during each worker sweep. Use these logs to check which credentials the gateway is watching, when the next refresh is due, and whether a credential is already refreshed.

```text
2026-05-16T19:42:34.705768Z  INFO openshell_server::provider_refresh: provider credential refresh worker sweep watched_count=1 due_count=0 rotation_requested_count=0
2026-05-16T19:42:34.705905Z  INFO openshell_server::provider_refresh: provider credential refresh watch provider=outlook-email credential_key=MS_GRAPH_ACCESS_TOKEN strategy=oauth2_refresh_token status=refreshed expires_at_ms=1778961995456 seconds_until_expiry=1440 next_refresh_at_ms=1778961395456 last_refresh_at_ms=1778958395456 seconds_until_refresh=840 due=false rotation_requested=false
```

The sweep line summarizes how many credential refresh records the worker inspected. The watch line shows the provider, credential key, strategy, refresh status, expiry time, next refresh time, and whether refresh is due or manually requested. It does not include access-token values or refresh material.

> **Note**
>
> Refresh updates the provider record. Sandboxes receive the updated credential through the same placeholder environment and proxy rewrite path as other provider credentials.

## Launch Sandboxes with Providers

Attach providers when creating a sandbox with repeated `--provider` flags:

```shell
openshell sandbox create \
  --name provider-demo \
  --provider work-claude \
  --provider work-github \
  -- claude
```

Each attached provider with a matching profile contributes a provider policy layer to the sandbox effective policy. The base policy is the user-authored sandbox policy that you can edit and apply. The effective policy is the composed policy that the sandbox enforces: base policy plus provider policy layers. A gateway-global policy suppresses provider-derived policy layers.

Updating a custom provider profile affects every provider instance whose `type` matches that profile ID. Provider instances are not rewritten, and sandbox-authored policies are not modified. Running sandboxes observe the updated provider-derived policy on their next config sync. If a gateway-global policy is active, provider-derived policy layers remain suppressed.

The CLI can infer a provider profile from a recognized sandbox command and
auto-create the provider from profile discovery. Attach a different existing
provider explicitly with `--provider`, or use `openshell sandbox provider
attach` after creation.

List providers attached to a sandbox:

```shell
openshell sandbox provider list provider-demo
```

The list output includes provider name, provider type, credential key count, and config key count.
Add `--output json` or `--output yaml` to return the provider name and type plus
sorted `credential_keys` and `config_keys` arrays. Structured output never
includes credential values, opaque credential handles, or config values.

## Policy Composition

OpenShell stores the base policy and provider attachments separately. When a sandbox asks for its effective policy, the gateway composes the current base policy with provider policy layers just in time.

For example, the GitHub example profile contains these endpoints and binaries:

```yaml wordWrap showLineNumbers={false}
id: github
display_name: GitHub
category: source_control
credentials:
  - name: api_token
    env_vars: [GITHUB_TOKEN, GH_TOKEN]
    required: true
    auth_style: bearer
    header_name: authorization
endpoints:
  - host: api.github.com
    port: 443
    protocol: rest
    access: read-only
    enforcement: enforce
  - host: api.github.com
    port: 443
    path: /graphql
    protocol: graphql
    access: read-only
    enforcement: enforce
  - host: github.com
    port: 443
    protocol: rest
    enforcement: enforce
    rules:
      - allow: { method: GET, path: "**" }
      - allow: { method: HEAD, path: "**" }
      - allow: { method: OPTIONS, path: "**" }
      - allow: { method: POST, path: "/**/git-upload-pack" }
binaries: [/usr/bin/gh, /usr/local/bin/gh, /usr/bin/git, /usr/local/bin/git]
```

The `github.com` git-transport endpoint uses explicit rules instead of the `read-only` preset so HTTPS clone and fetch work out of the box. Git smart HTTP performs a `GET` on `*/info/refs` followed by a `POST` to `*/git-upload-pack`; the read-only preset (`GET`/`HEAD`/`OPTIONS`) blocks that `POST`. The rules permit the read-only methods plus `POST */git-upload-pack` only, so clone and fetch succeed while push (`git-receive-pack`) and other API mutations stay denied. Enabling push requires an explicit policy proposal.

If a sandbox attaches a provider named `work-github`, the effective policy includes a generated provider rule:

```yaml wordWrap showLineNumbers={false}
network_policies:
  custom_pypi:
    name: custom_pypi
    endpoints:
      - host: pypi.org
        port: 443
        protocol: rest
        access: read-only
        enforcement: enforce
    binaries:
      - path: /usr/bin/python

  _provider_work_github:
    name: _provider_work_github
    endpoints:
      - host: api.github.com
        port: 443
        protocol: rest
        access: read-only
        enforcement: enforce
      - host: api.github.com
        port: 443
        path: /graphql
        protocol: graphql
        access: read-only
        enforcement: enforce
      - host: github.com
        port: 443
        protocol: rest
        enforcement: enforce
        rules:
          - allow: { method: GET, path: "**" }
          - allow: { method: HEAD, path: "**" }
          - allow: { method: OPTIONS, path: "**" }
          - allow: { method: POST, path: "/**/git-upload-pack" }
    binaries:
      - path: /usr/bin/gh
      - path: /usr/local/bin/gh
      - path: /usr/bin/git
      - path: /usr/local/bin/git
```

Inspect the effective policy:

```shell
openshell policy get provider-demo --full
```

Pull the round-trippable base policy without provider entries:

```shell
openshell policy get provider-demo --base
```

Composition follows these rules:

* Provider policy entries use reserved `_provider_*` keys derived from provider instance names.
* Provider policy entries are derived data. OpenShell does not persist them back into the base policy.
* User-authored policies cannot define `_provider_*` network policy keys. The gateway rejects those keys on sandbox create and full policy replacement, and sandbox-originated policy sync strips them before persistence.
* Provider and user rules are concatenated. Overlapping endpoints remain separate rules.
* A gateway global policy override suppresses provider-derived policy layers.

## Attach and Detach Providers

Attach an existing provider and wait until the sandbox can use it:

```shell
openshell sandbox provider attach provider-demo work-github --wait --timeout 30
```

Detach a provider and wait until the sandbox stops resolving its credentials:

```shell
openshell sandbox provider detach provider-demo work-github --wait --timeout 30
```

Attach and detach are idempotent. Attach validates that the provider exists before mutating the sandbox, and provider deletion fails while the provider is attached to any sandbox.

### Inspect Provider Readiness

Without `--wait`, attach, detach, and update confirm that the gateway saved the change. Add `--wait` when the next step depends on that change taking effect. The command succeeds after the sandbox applies the matching credentials and policy, and updates the environment used by new processes.

Each result includes a change record called a `receipt` in the API. Its ID lets you check the same change later, including after a timeout:

```shell
openshell sandbox provider status provider-demo work-github --output json
openshell sandbox provider status provider-demo work-github --receipt RECEIPT_ID --wait --timeout 30
```

Replace `RECEIPT_ID` with the `receipt_id` returned by attach, detach, or update. The ID always refers to the original change. If a later change replaces it, the status is `superseded` and the wait ends without reporting success.

If attach, detach, or update reports `CONFIG_OPERATION_STORAGE_UNCERTAIN`, the change may already be saved even though the gateway could not record its readiness receipt. Do not blindly retry the mutation: inspect the provider and sandbox state and reconcile the saved change first. A receipt may be unavailable, so an error does not establish that the mutation was rolled back or that the sandbox is ready.

Readiness states have these meanings:

* `persisted`: the gateway saved the change; the command has not checked the sandbox yet.
* `pending`: the sandbox has not confirmed all parts of the change.
* `ready`: the sandbox applied the requested provider credentials, policy, and environment for new processes.
* `revoked`: the detached provider's credentials no longer resolve, and new processes do not receive its credential references.
* `withheld`: a credential or supervisor configuration prevents the sandbox from applying the change.
* `failed`: the sandbox could not install the credentials, policy, or process environment.
* `superseded`: a later change replaced the original request.

The default wait is 30 seconds and the maximum is 3600 seconds. The timeout starts after the gateway saves the change and includes time spent requesting status. If time runs out, the command exits with an error and reports `wait_outcome: timed_out` with the last known state. The change remains saved and may finish applying later. Check its ID again to learn the result.

An unavailable supervisor, an expired report, a failed installation, or a missing process-environment acknowledgment cannot produce `ready` or `revoked`. After a reconnect or restart, the current authenticated supervisor must report its installed state again.

JSON and YAML output include change IDs, requested and installed revisions, timestamps, reason categories, and a result for each sandbox. Revision strings identify configurations; compare them for equality rather than numerical order. Status output excludes credential values, credential references, authorization headers, and raw installation errors.

The `persisted_time`, `observed_time`, and `evaluated_time` fields use RFC 3339 timestamp strings. An absent `observed_time` means the current supervisor session has not supplied accepted evidence. API operation timestamps use protobuf `Timestamp`, and report intervals and observation lifetimes use protobuf `Duration`, following the [protobuf time representation](/reference/protobuf-time-types).

The API's `operation` field records the common operation's historical outcome; its `operation_id` equals the receipt ID. Use the provider `state` and the wait result for current readiness. A disconnected supervisor can make the live state pending even after the operation previously applied, and a newer change can supersede the live result. Historical completion does not override those checks.

### Runtime Limitations

Running sandboxes periodically check for provider and policy changes. Use `--wait` or `sandbox provider status` to confirm when a saved change has taken effect.

The policy effect applies to future effective policy reads after the sandbox observes the update. The credential environment effect applies only to new process launches after the update is observed, such as later SSH, exec, or SFTP sessions.

Already-running processes keep the placeholder environment they started with. For ordinary static credentials, an existing reference retains its selected revision after a value update; readiness does not retarget that reference to the replacement value. After an update wait succeeds, launch a new process to receive the updated credential reference. Gateway-managed refresh follows its own reference lifecycle. Expiry, endpoint authorization, and acknowledged detach continue to apply when the proxy resolves retained references.

For a static provider, the sequence is attach, wait, then launch client A; update, wait, then launch client B. B's first request uses the newly installed credential. Readiness does not establish that A has stopped using its retained revision or that an old upstream key can be retired.

An acknowledged detachment removes its provider policy layer from the active effective policy, revokes future resolution for its existing placeholders, and removes its credential placeholders from future process environments. It does not remove strings from already-running process environments or undo requests already forwarded upstream.

OpenShell rejects provider updates and refresh configuration when they would make two providers attached to the same sandbox expose the same active credential environment key. It also rejects attached provider sets with ambiguous dynamic token grants at equal host/path specificity. Use provider-specific credential names and make one dynamic grant selector more specific when one sandbox needs multiple providers with overlapping upstream concepts.

## Next Steps

* Use [Providers](/sandboxes/manage-providers) for the current provider command reference.
* Use [Customize Sandbox Policies](/sandboxes/policies) to apply user-authored policy rules.
* Use [Policy Schema Reference](/reference/policy-schema) for endpoint and L7 rule field details.