Table of Contents

Providers and profiles

A provider holds the credentials and configuration for an external service (a source-control host, an inference API, a data source). Attached to a sandbox, it makes those credentials available to the workload under the gateway's control. A provider profile describes a provider type: its credentials, how they are injected, the endpoints and binaries they are for, and how they are refreshed.

Client Area
client.Providers (IProviderClient) Create, get, list, update, delete, and ensure providers; export runtime credentials.
client.SandboxProviders (ISandboxProviderClient) Attach and detach providers, and wait until a sandbox has installed them.
client.Providers.Profiles (IProviderProfileClient) List, get, import, update, lint, and delete profiles.
client.Providers.Refresh (IProviderRefreshClient) Gateway-owned credential refresh: status, configure, rotate, delete.

Create and list providers

// Creates the provider, or updates it in place when one with the same name exists.
var provider = await client.Providers.EnsureAsync(new Provider
{
    Name = "github",
    Type = "github",
    Credentials = new Dictionary<string, string>
    {
        ["GITHUB_TOKEN"] = Environment.GetEnvironmentVariable("GITHUB_TOKEN")!,
    },
    Labels = new Dictionary<string, string> { ["team"] = "platform" },
});

// Provider.ToString() never prints credential values.
Console.WriteLine(provider);

await foreach (var p in client.Providers.List(new ListOptions { LabelSelector = "team=platform" }))
{
    Console.WriteLine($"{p.Name} ({p.Type})");
}

EnsureAsync creates the provider or updates it in place; CreateAsync fails with AlreadyExists when it exists. Provider, ProviderCredentialValue, and RefreshConfig override ToString so secret values never print.

Attach to a sandbox

Attaching returns a ProviderMutationReceipt that names the exact authority the sandbox should install. WaitAsync waits, within a timeout of at most one hour, until the sandbox supervisor reports it:

var attached = await client.SandboxProviders.AttachAsync("agent-1", "github");
if (attached.Receipt is { } receipt)
{
    // Wait until the sandbox supervisor has installed exactly this authority.
    var wait = await client.SandboxProviders.WaitAsync(receipt, TimeSpan.FromMinutes(1));
    switch (wait.Outcome)
    {
        case ProviderWaitOutcome.Complete:
            break;
        case ProviderWaitOutcome.TimedOut:
            Console.WriteLine($"still {wait.Status.State}: {wait.Status.Reason}");
            break;
        case ProviderWaitOutcome.Terminal:
            throw new InvalidOperationException($"provider not installed: {wait.Status.Reason}");
    }
}

await foreach (var p in client.SandboxProviders.List("agent-1"))
{
    Console.WriteLine(p.Name);
}
ProviderWaitOutcome Meaning
Complete The authority was installed (attach, update) or revoked (detach).
TimedOut The deadline passed; Status is the last state observed.
Terminal Installation failed or was withheld, or a later mutation superseded this one. It never turns into success.

Detaching returns a receipt too; revocation is complete only when it reports Revoked. Providers can also be attached at creation through SandboxSpec.Providers.

Update credentials

UpdateAsync returns one receipt per sandbox the provider is attached to; WaitAllAsync waits for all of them under one deadline.

var current = await client.Providers.GetAsync("github");
var update = await client.Providers.UpdateAsync(current with
{
    Credentials = new Dictionary<string, string>
    {
        ["GITHUB_TOKEN"] = Environment.GetEnvironmentVariable("GITHUB_TOKEN_ROTATED")!,
    },
});

// One receipt per sandbox the provider is attached to, awaited under one deadline.
var results = await client.SandboxProviders.WaitAllAsync(update.TargetReceipts, TimeSpan.FromMinutes(2));

Export runtime credentials

GetCredentialsAsync exports exactly the credential keys you name, or fails without a result. It requires a client authenticated with a trusted operator client certificate (OU=operator) and no bearer or edge token, and an explicit workspace. The gateway refreshes eligible missing or short-lived values first. Keep the values out of logs.

// Requires a client authenticated with an operator client certificate (OU=operator) and no bearer token.
var credentials = await operatorClient.Providers.GetCredentialsAsync(
    "my-graph",
    ["MS_GRAPH_ACCESS_TOKEN"],
    new ProviderCredentialsOptions
    {
        Workspace = "production",
        MinimumRemainingLifetime = TimeSpan.FromMinutes(5),
    });

var token = credentials["MS_GRAPH_ACCESS_TOKEN"];
Console.WriteLine($"expires {token.ExpirationTime}");   // never log token.Value

Profiles

Profiles live in a workspace; Profiles.Platform is a view of the platform (global) profiles. LintAsync reports diagnostics without changing anything, ImportAsync adds profiles, and UpdateAsync replaces one by id, with a compare-and-swap on its resource version.

var profile = new ProviderProfile
{
    Id = "internal-api",
    DisplayName = "Internal API",
    Category = ProviderProfileCategory.Other,
    Credentials =
    [
        new ProfileCredential
        {
            Name = "api_key",
            EnvVars = ["INTERNAL_API_KEY"],
            Required = true,
            StablePlaceholder = true,   // one placeholder value across value-only revisions
        },
    ],
};

// Lint reports diagnostics without importing anything.
var lint = await client.Providers.Profiles.LintAsync([new ProfileImportItem(profile)]);
foreach (var d in lint.Diagnostics)
{
    Console.WriteLine($"{d.Severity} {d.ProfileId}.{d.Field}: {d.Message}");
}

if (lint.Valid)
{
    var imported = await client.Providers.Profiles.ImportAsync([new ProfileImportItem(profile)]);
    Console.WriteLine($"imported: {imported.Imported}");
}

// Platform (global) profiles instead of the ones in this workspace.
await foreach (var builtin in client.Providers.Profiles.Platform.List())
{
    Console.WriteLine($"{builtin.Id}: {builtin.DisplayName}");
}

Stable placeholders

Provider credentials reach the sandbox environment as opaque placeholders. By default each provider revision gets a new placeholder. A profile credential with StablePlaceholder = true keeps one identity-bound placeholder across value-only revisions, so the sandbox environment does not change when only the secret value does.

A stable placeholder needs at least one environment variable (EnvVars), and cannot be combined with gateway-managed refresh or a token grant. Because older gateways drop unknown fields silently, ImportAsync, UpdateAsync, and LintAsync first check that the gateway supports it and fail with OpenShellErrorKind.Unimplemented when it does not; LintProfilesResult.SupportsStablePlaceholder reports support.

Credential refresh

For credentials the gateway refreshes itself (for example OAuth2 tokens), Refresh reads the state of each credential, configures the strategy, forces a rotation, and removes the configuration.

foreach (var status in await client.Providers.Refresh.GetStatusAsync("my-graph"))
{
    Console.WriteLine(status);
}

// Rotate a gateway-refreshed credential now.
await client.Providers.Refresh.RotateAsync("my-graph", "MS_GRAPH_ACCESS_TOKEN");