Repository navigation
feat(providers): add gateway-owned credential refresh for short-lived provider tokens #1306
Description
Activity
- addedarea:cliCLI-related workCLI-related workarea:inferenceInference routing and configuration workInference routing and configuration workarea:supervisorProxy and routing-path workProxy and routing-path workarea:gatewayGateway server and control-plane workGateway server and control-plane worktest:e2eRequires end-to-end coverageRequires end-to-end coverage
on May 11, 2026 🏗️ build-plan
Implementation Plan
Issue type:
feat
Complexity: High
Confidence: Medium-High — the storage, refresh ownership, external strategy, and profile metadata boundaries are now decided; the remaining risk is implementation breadth across gateway, CLI, scheduler, and tests.Summary
Add gateway-owned refresh for short-lived provider tokens by keeping
Provider.credentialsas the current injectable access-token map and moving refresh material into a separate non-injectable object stored in the existing objects table. The gateway refresh worker should update only the provider record on successful refresh so the existingGetSandboxProviderEnvironmentrevision flow, sandboxProviderCredentialState, and inference route resolution continue to work without sandbox-side redesign.Resolved Design Decisions
externalrefresh is push-only: OpenShell does not fetch from external brokers or run scripts. External systems update current credentials through the provider update path.provider updateremains the path for externally rotated current credentials, with a new general expiry option such as--credential-expires-at KEY=TIMESTAMP.- Credential expiry metadata is general for any provider credential and must apply to static, external, and gateway-refreshed credentials.
provider rotatemeans “force gateway-owned refresh now” and applies to gateway-managed strategies, not normal external refresh pushes.- Refresh metadata lives on
ProviderProfileCredential, because refresh lifecycle is per credential rather than per provider profile. - User-facing refresh commands address credentials by injectable credential key, for example
MS_GRAPH_ACCESS_TOKEN, not by profile-internal credential name such asaccess_token. - The gateway maps an injectable key back to the matching
ProviderProfileCredentialby loading the provider profile and finding the credential whoseenv_varscontains that key. - A new provider credential refresh-state object owns runtime refresh material/status and links back to the provider through
objects.scope = provider_id; the payload also storesprovider_id,provider_name, andcredential_keyfor self-description. objects.scopeis a logical/application-enforced FK to the provider instance, not a database FK.- Expiry must be enforced during credential resolution, not only displayed in status. Expired refresh-managed credentials should fail closed even if old sandbox placeholder generations still exist.
Codebase Mapping
proto/datamodel.proto,proto/openshell.proto: add the new stored provider-refresh object, refresh status/config RPCs, provider profile refresh metadata, and per-credential expiry metadata surfaces.crates/openshell-core/src/metadata.rs: register object traits for the new stored refresh object.crates/openshell-providers/src/profiles.rs,providers/*.yaml: extend profile schema and validation withProviderProfileCredential.refresh; seed Microsoft and Google defaults first.crates/openshell-server/src/grpc/provider.rs,crates/openshell-server/src/grpc/mod.rs, newcrates/openshell-server/src/provider_refresh.rs,crates/openshell-server/src/lib.rs: persist refresh material/status, expose refresh UX RPCs, run the scheduler, and write refreshed access tokens back intoProvider.credentials.crates/openshell-server/src/grpc/policy.rs,crates/openshell-server/src/inference.rs: reuse existing provider-env and inference resolution; add expiry-aware credential resolution and regression coverage proving provider updates trigger new revisions and refreshed inference credentials.crates/openshell-cli/src/main.rs,crates/openshell-cli/src/run.rs,crates/openshell-cli/tests/*: addrefresh-status,rotate,refresh-config, and provider update expiry support while keeping refresh material separate from normal injectable credentials.
Implementation Steps
- Define the data model and API surface: add a dedicated non-injectable provider-refresh object in the existing objects table, add refresh strategy metadata to
ProviderProfileCredential, add per-credential expiry metadata, and add minimal RPCs forrefresh-status,refresh-config, androtate. - Implement gateway persistence and validation: store refresh material and status separately from
Provider.credentials, useobjects.scope = provider_idfor refresh-state ownership, preserve existing provider read contracts, and keepCreateProvider/UpdateProviderscoped to injectable current credentials plus general credential expiry metadata. - Add provider update expiry support: allow current credentials to be updated with optional
credential_key -> expires_atmetadata, and ensure expiry metadata can be set for any credential without changing theProvider.credentialsmap shape. - Add the refresh engine: introduce a background gateway worker that scans configured refresh objects, schedules refresh before expiry, serializes refresh per provider credential, and implements
static,external,oauth2_refresh_token,oauth2_client_credentials, andgoogle_service_account_jwt, with first-pass coverage for Microsoft S2S/Entra, Google OAuth refresh-token, and Google service-account/Vertex. - Reuse the existing credential propagation path: on successful gateway-owned refresh, update
Provider.credentials[credential_key]and expiry metadata so existing provider env revision polling, sandbox placeholder rotation, and inference-route resolution observe new credentials without mutating running process envs. - Enforce expiry at resolution time: expired credentials should not resolve into sandbox requests, including retained old placeholder generations, and should return clear fail-closed diagnostics.
- Wire CLI UX: add
openshell provider refresh-status,openshell provider refresh-config, andopenshell provider rotate; addprovider update --credential-expires-at KEY=TIMESTAMP; use injectable credential keys in user-facing command arguments. - Finish with targeted profile and docs updates: add refresh metadata to prioritized profiles first, then document the new refresh lifecycle and user-facing commands.
Test Plan
- Unit tests: profile serde/validation for refresh metadata; new object metadata/accessors; strategy-specific refresh helpers; scheduler timing/backoff/state transitions; redaction tests proving refresh material never appears in provider read APIs.
- Integration tests: gateway provider tests for update vs refresh-config separation, successful refresh updating
Provider.credentials, failed refresh preserving last-known-good credentials until expiry, expiry fail-closed behavior,compute_provider_env_revisionchanging when the provider record changes, and inference route resolution seeing rotated provider tokens. - CLI integration tests: parsing and execution of new provider refresh commands,
provider update --credential-expires-at, injectable-key-based credential selection, and clear error cases for invalid refresh-config/material input. - E2E tests: one running sandbox attached to a refresh-managed provider, backed by a fake token issuer, proving the gateway refresh updates the provider record and the sandbox observes the new
provider_env_revisionthrough the existing poll loop; add inference-route coverage if the refreshed provider is used for managed routes.
Docs Impact
- Update
architecture/gateway.mdwith the provider refresh ownership boundary and background worker lifecycle. - Update
docs/sandboxes/manage-providers.mdxfor refresh configuration, status, rotation, external push refresh, expiry metadata, and supported strategies. - Update
docs/sandboxes/inference-routing.mdxif refreshed provider credentials become part of the supported inference path.
Security Risks
CWE-200: refresh tokens, client secrets, and service-account keys must stay non-injectable, redacted from all read APIs, and excluded from logs and OCSF messages.CWE-918: if token endpoints or issuer URLs are configurable, validate them against profile metadata or an explicit allowlist so the gateway refresh worker cannot become an SSRF primitive.CWE-362: refresh must be single-writer per provider credential; failed or concurrent refreshes must not clobber the current credential map or cause unnecessary revision churn.- Expiry enforcement must account for retained sandbox credential generations so stale tokens do not remain usable after their recorded expiry.
Revision 2 — resolved external strategy, ProviderProfileCredential metadata, provider update expiry, refresh-state ownership, and expiry enforcement decisions
Revision 1 — initial plan- addedstate:review-readyReady for human reviewReady for human reviewstate:agent-readyApproved for agent implementationApproved for agent implementationand removed
on May 11, 2026 - addedstate:in-progressWork is currently in progressWork is currently in progressstate:pr-openedPR has been opened for this issuePR has been opened for this issue
on May 13, 2026 We are hitting this in a downstream integration (a desktop app that manages OpenShell sandboxes in WSL), adding a data point and a question on the build plan.
The #1349 foundation works well for us (gateway-owned rotation of Azure DevOps OAuth tokens via ConfigureProviderRefresh). The gap we still see on v0.0.90 is that after the gateway rotates a credential, long-lived processes inside the sandbox (VS Code server, open PTYs) hold their originally captured placeholder and their git operations fail from then on, the egress proxy drops the TLS connection, so users see opaque connection failures mid-session.
Our workaround is a "kill VS Code server" button so users can restart it to re-capture placeholders.One question on the plan, after a successful refresh, does a placeholder captured at sandbox start keep resolving to the current credential indefinitely? i.e. is recognition of old markers unbounded (keyed per credential) or a bounded recent-generations list? Our tokens rotate roughly hourly and editor/PTY processes could live for days, so if old markers either stay bound to their original token value or eventually fall off a bounded retention window, processes started before a rotation still break, just later.
@tarabishy2020 looks like your issue should have been solved by #2777 - will close out this issue and let us know if you see the issue again.
Problem Statement
OpenShell needs first-class gateway-owned credential refresh for providers that use short-lived access tokens. The current provider model can inject static credentials into sandbox traffic, and recent provider work added provider profiles, attach/detach, custom profiles, and provider environment revision polling for running sandboxes. However, OpenShell does not yet own the lifecycle for refresh material such as OAuth refresh tokens, client secrets, or service-account private keys.
This blocks long-running sandboxes that depend on providers such as Microsoft Graph / Azure / Entra-backed APIs, Google Workspace, Google Vertex AI, IBM watsonx, ChatGPT/OpenAI OAuth-style integrations, and other enterprise SSO-bound APIs. Access tokens commonly expire during a sandbox session. Users should not have to restart sandboxes, expose refresh material inside the sandbox, or build one-off host refresh daemons.
Refresh material must stay in the OpenShell control plane / gateway. Sandboxes should continue to see only placeholder values or proxy-injected short-lived access tokens, and should observe refreshed access tokens through the existing provider environment revision path.
Proposed Design
Add gateway-owned provider credential refresh while keeping placeholder-based credential injection as the delivery path for this first chunk.
The core model should be:
Provider.credentialsas the injectable current credential map used byGetSandboxProviderEnvironment.ProviderCredentialStatemechanics so running sandboxes observe refreshed credentials without restarting.Provider.credentialsand out ofGetSandboxProviderEnvironment.Initial refresh strategies should include:
static: no active minting. Provider credential updates are picked up through the current provider environment revision path.external: OpenShell does not mint tokens. An external process updates the provider credential store, and running sandboxes pick up the update through the revision path.oauth2_refresh_token: gateway refreshes an access token using stored refresh-token material.oauth2_client_credentials: gateway mints access tokens using client credentials, covering Microsoft S2S / Entra-style flows.google_service_account_jwt: gateway signs a JWT assertion using stored service-account material and exchanges it for a Google access token, covering Vertex/GCP service-account use cases.The first implementation should prioritize Microsoft S2S / Entra, Google OAuth refresh-token, and Google service-account / Vertex flows. The design should leave room for IBM watsonx IAM, AWS STS, OIDC variants, and other future refresh strategies without coupling them to the first implementation.
User-facing UX should include a way to inspect refresh status, force rotation, and update refresh configuration without exposing refresh material in normal provider output. Candidate CLI shape:
Provider create/update should accept refresh material separately from injectable credentials, so users cannot accidentally place refresh tokens or service-account keys in the sandbox-visible credential map.
Related Requirements
Alternatives Considered
Provider.credentials. Rejected becauseProvider.credentialsis the injectable map returned throughGetSandboxProviderEnvironment; refresh material must not enter the sandbox.Agent Investigation
The current repo already has much of the sandbox-side propagation path needed for refreshed credentials:
Provider.credentialsis the current secret map on provider records.GetSandboxProviderEnvironmentresolves attached providers into an environment map and returnsprovider_env_revision.crates/openshell-sandbox/src/provider_credentials.rsmaintains revision-scoped credential snapshots and keeps recent generations so existing placeholders continue to resolve.provider_env_revisionchanges and installs the new environment intoProviderCredentialState.SecretResolver, so updated provider credential values can affect later proxied requests without mutating already-running process environments.Non-Goals