# Configure credentials

> Credential providers (file secrets, keychain, 1Password, encrypted stores), connection creation payloads, OAuth flows, and placement-based auth templates.

- Repository: RhysSullivan/executor
- GitHub: https://github.com/RhysSullivan/executor
- Human docs: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052
- Complete Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/llms-full.txt

## Source Files

- `packages/core/api/src/oauth/api.ts`
- `packages/core/api/src/handlers/oauth.ts`
- `packages/core/sdk/src/oauth-service.ts`
- `packages/plugins/file-secrets/src/sdk/plugin.ts`
- `packages/plugins/keychain/src/sdk/plugin.ts`
- `packages/plugins/onepassword/src/sdk/plugin.ts`
- `packages/core/sdk/src/http-auth.ts`

---

---
title: "Configure credentials"
description: "Credential providers (file secrets, keychain, 1Password, encrypted stores), connection creation payloads, OAuth flows, and placement-based auth templates."
---

Executor stores credentials as **connections**: owner-scoped bindings to one integration, one auth template, and one credential provider. Static secrets are created with `connections.create` (HTTP `POST /connections`, SDK `executor.connections.create`); OAuth connections are minted through `oauth.start` / `oauth.complete`. Protocol plugins declare **placement-based auth templates** via `@executor-js/sdk/http-auth`; the runtime resolves credential values lazily per tool call.

## Credential providers

A `CredentialProvider` is where secret material lives. The connection row stores routing (`provider` key plus opaque `item_ids` per template variable), not the secret itself. Providers register at executor startup from `createExecutor({ providers: [...] })` and from each plugin's `credentialProviders` hook. Config-level providers register first and the **first writable provider** becomes the default store for pasted `value` / `values` inputs.

| Provider key | Package | Writable | Storage | `list` support |
|---|---|---|---|---|
| `file` | `@executor-js/plugin-file-secrets` | yes | Flat JSON map at `{XDG_DATA_HOME}/executor/auth.json` (mode `0600`) | yes |
| `keychain` | `@executor-js/plugin-keychain` | yes | OS keychain (`@napi-rs/keyring`, service name default `executor`) | no |
| `onepassword` | `@executor-js/plugin-onepassword` | no (read-only) | `op://` URIs scoped to a configured vault | yes |
| `workos-vault` | `@executor-js/plugin-workos-vault` | yes | WorkOS Vault (KEK-partitioned encrypted objects) | yes |

<Note>
The keychain plugin probes write/delete at startup. If the platform keychain is unreachable (headless CI, WSL without secret-service), the provider is skipped rather than registered.
</Note>

### File secrets (`file`)

Secrets are a flat map of opaque id to value. The v2 model drops per-scope partitioning; the connection row owns the `(tenant, owner, subject)` partition.

```json
{
  "github-token": "ghp_xxx",
  "connection:org:inventory:default:token": "inventory-api-key"
}
```

Override the directory with `fileSecretsPlugin({ directory: "/path/to/dir" })`. The extension exposes `executor.fileSecrets.filePath`.

### Keychain (`keychain`)

Each `ProviderItemId` is the keychain account name. Override the service name with `keychainPlugin({ serviceName: "my-app" })`.

### 1Password (`onepassword`)

Configure once per owner partition via the plugin tools (`onepassword.configure`) or extension API. Auth modes:

- `desktop-app` with `accountName` (local biometric)
- `service-account` with `token`

Item ids are either fully qualified `op://vault/item/field` URIs or bare item ids scoped to the configured vault. The provider resolves secrets on read; it never writes.

### WorkOS Vault (`workos-vault`)

Cloud-hosted encrypted storage for self-host and Executor Cloud. Requires `workosVaultPlugin({ credentials: { apiKey, clientId } })` or a pre-built client. Object names encode owner partitions (`connection:<owner>:...`, `oauth:<owner>:...`).

### Register providers in application code

<CodeGroup>
```typescript title="SDK (promise mode)"
import { createExecutor, ProviderKey, ProviderItemId, Effect } from "@executor-js/sdk/promise";
import { fileSecretsPlugin, keychainPlugin, onepasswordPlugin } from "...";

const memory = new Map<string, string>();
const memoryProvider = {
  key: ProviderKey.make("memory"),
  writable: true,
  get: (id) => Effect.sync(() => memory.get(String(id)) ?? null),
  set: (id, value) => Effect.sync(() => { memory.set(String(id), value); }),
};

const executor = await createExecutor({
  plugins: [fileSecretsPlugin(), keychainPlugin(), onepasswordPlugin()],
  providers: [memoryProvider], // registers first; wins as default writable
});
```

```typescript title="all-plugins reference"
// examples/all-plugins wires every published provider + protocol plugin.
// Uncomment workosVaultPlugin when WORKOS_API_KEY is available.
```
</CodeGroup>

Discover registered backends:

| Endpoint | Returns |
|---|---|
| `GET /providers` | `ProviderKey[]` |
| `GET /providers/:key/items` | `{ id, name }[]` for browse-and-pick UIs |

## Connection creation payloads

A connection is identified by `(owner, integration, name)` where `owner` is `org` (workspace-shared) or `user` (personal). The `template` field selects one auth method slug from the integration catalog.

### Credential origin (exactly one)

The HTTP API enforces one origin per create request. The SDK accepts the same shapes plus a per-variable `inputs` map.

<ParamField body="value" type="string">
Single pasted secret. Sugar for the `token` variable. Written to the default writable provider at `connection:{owner}:{integration}:{name}:token`.
</ParamField>

<ParamField body="values" type="Record<string, string>">
One pasted value per placement variable. Required for multi-input methods (for example Datadog `api_token` + `team_id`).
</ParamField>

<ParamField body="from" type="{ provider: ProviderKey; id: ProviderItemId }">
Reference an external provider entry. The value is resolved on demand; Executor stores only the opaque id.
</ParamField>

<ParamField body="inputs" type="Record<string, ConnectionInputOrigin>" required={false}>
SDK-only canonical form. Mixes `{ value }` and `{ from: { provider, id } }` per variable. HTTP create does not expose this field yet.
</ParamField>

### Common fields

<ParamField body="owner" type="Owner" required>
`org` or `user`. Personal connections require a signed-in subject.
</ParamField>

<ParamField body="name" type="ConnectionName" required>
Unique per `(owner, integration)`.
</ParamField>

<ParamField body="integration" type="IntegrationSlug" required>
Catalog slug for the OpenAPI, GraphQL, or MCP source.
</ParamField>

<ParamField body="template" type="AuthTemplateSlug" required>
Auth method slug (`apiKey`, `bearer`, `oauth2`, `none`, ...).
</ParamField>

<ParamField body="identityLabel" type="string | null">
Optional human label (which account).
</ParamField>

<ParamField body="description" type="string | null">
Agent-visible context surfaced in `connections.list` and execute-tool inventory.
</ParamField>

### Constraints

- A credentialed template requires at least one input. Exception: `template: "none"` for public integrations (empty `values: {}` is valid).
- Cannot mix pasted and external inputs in one connection.
- All external inputs must use the same provider.
- OAuth connections are minted via `oauth.start`, not `connections.create`.

<RequestExample>
```json title="POST /connections — single API key"
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "value": "inventory-api-key"
}
```
</RequestExample>

<RequestExample>
```json title="POST /connections — multi-input method"
{
  "owner": "org",
  "name": "mixed",
  "integration": "datadog",
  "template": "token_and_team",
  "values": {
    "api_token": "tok_mixed",
    "team_id": "team_42"
  }
}
```
</RequestExample>

<RequestExample>
```json title="POST /connections — external provider reference"
{
  "owner": "org",
  "name": "prod",
  "integration": "github",
  "template": "apiKey",
  "from": {
    "provider": "onepassword",
    "id": "op://VaultName/GitHub/item/password"
  }
}
```
</RequestExample>

<ResponseExample>
```json title="Connection response"
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "provider": "file",
  "address": "tools.inventory.org.default",
  "identityLabel": null,
  "description": null,
  "expiresAt": null,
  "oauthClient": null,
  "oauthClientOwner": null,
  "oauthScope": null
}
```
</ResponseExample>

Creating a connection writes the credential, persists the connection row, and **produces per-connection tools** for that integration.

## Placement-based auth templates

Protocol plugins (OpenAPI, GraphQL, MCP) share the `@executor-js/sdk/http-auth` vocabulary. An integration declares a **list** of auth methods; a connection binds one by `template` slug.

### ApiKey methods

Each `apikey` method carries `placements`: spots on the outbound HTTP request.

| Placement field | Role |
|---|---|
| `carrier` | `header` or `query` |
| `name` | Header or query param name |
| `prefix` | Literal prepended to the credential (for example `Bearer `) |
| `variable` | Credential input name; defaults to `token` |
| `literal` | Static value with no credential input |

Core resolves `values: Record<variable, string|null>` and plugins render via `renderAuthPlacements`. Use `requiredPlacementVariables(placements)` to know which inputs a connection must supply.

### Authoring dialect

Plugin registration inputs accept a request-shaped template normalized at the boundary:

```typescript
import { variable } from "@executor-js/sdk/http-auth";

authenticationTemplate: [
  {
    slug: "token_and_team",
    type: "apiKey",
    headers: { Authorization: ["Bearer ", variable("api_token")] },
    queryParams: { team_id: [variable("team_id")] },
  },
  { slug: "bearer", type: "apiKey", headers: { Authorization: ["Bearer ", variable("token")] } },
  { kind: "oauth2" }, // OAuth is per-plugin, not in placements model
]
```

Rules for template values:

- A parts-array renders at most **one** variable, and it must be the **final** part.
- Two placements sharing a `variable` share one credential input.
- Distinct variables get distinct inputs (and a `values` map at connect time).

### OAuth methods

OAuth is **not** modeled as placements. Each protocol plugin carries its own OAuth variant (`kind: "oauth2"` for OpenAPI/MCP, with plugin-specific endpoint/scope fields). OAuth-refreshed connections resolve only the `token` input; do not mix OAuth token values into apikey placement methods.

### None auth

`{ slug: "none", kind: "none" }` integrations need no credential. Create with `template: "none"` and no value origin.

## OAuth flows

OAuth is a credential mechanism separate from integration type. The v2 model:

1. **Register an OAuth app** (`oauth_client` row) with endpoints and client credentials.
2. **Start a flow** through that app to mint a connection for one integration.
3. **Complete** the authorization code exchange (or connect inline for `client_credentials`).

```mermaid
sequenceDiagram
  participant UI as Web UI / client
  participant API as Executor HTTP API
  participant OAuth as oauth service
  participant AS as Authorization server
  participant CP as Credential provider

  UI->>API: POST /oauth/clients (createClient)
  API->>OAuth: createClient
  OAuth->>CP: set oauth-client:{owner}:{slug}:secret
  UI->>API: POST /oauth/start
  API->>OAuth: start
  alt client_credentials
    OAuth->>AS: token exchange
    OAuth->>CP: set oauth:{owner}:{integration}:{name}
    OAuth-->>UI: { status: "connected", connection }
  else authorization_code
    OAuth->>OAuth: persist oauth_session + PKCE
    OAuth-->>UI: { status: "redirect", authorizationUrl, state }
    UI->>AS: user authorizes
    AS->>API: GET /oauth/callback?state&code
    API->>OAuth: complete
    OAuth->>AS: code exchange
    OAuth->>CP: set access + refresh tokens
    OAuth-->>UI: connection (via popup postMessage)
  end
```

### OAuth HTTP endpoints

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/oauth/clients` | Register owner-scoped OAuth app |
| `POST` | `/oauth/clients/register-dynamic` | RFC 7591 Dynamic Client Registration |
| `GET` | `/oauth/clients` | List visible apps (no secrets) |
| `DELETE` | `/oauth/clients/:slug` | Remove app (connections fail at next refresh) |
| `POST` | `/oauth/start` | Begin flow; returns `connected` or `redirect` |
| `POST` | `/oauth/complete` | Exchange code, mint connection |
| `POST` | `/oauth/cancel` | Drop in-flight session |
| `POST` | `/oauth/probe` | Discover AS metadata for onboarding |
| `GET` | `/oauth/callback` | Browser callback; renders popup HTML |

### Start payload

<ParamField body="client" type="OAuthClientSlug" required>
OAuth app slug.
</ParamField>

<ParamField body="clientOwner" type="Owner" required>
Owner of the OAuth app. A personal connection may use a shared workspace app (`clientOwner: "org"`, `owner: "user"`). Workspace connections cannot use a personal app.
</ParamField>

<ParamField body="owner" type="Owner" required>
Connection owner to mint.
</ParamField>

<ParamField body="name" type="ConnectionName" required>
Connection name.
</ParamField>

<ParamField body="integration" type="IntegrationSlug" required>
Target integration.
</ParamField>

<ParamField body="template" type="AuthTemplateSlug" required>
OAuth template slug (for example `oauth2`).
</ParamField>

<ParamField body="redirectUri" type="string | null">
Per-flow override. Defaults to executor `redirectUri` (`${webBaseUrl}${mountPrefix}/oauth/callback`). Required for `authorization_code` flows; hosts without a callback must fail loudly.
</ParamField>

Requested scopes come from the integration's **declared** OAuth scopes for `(integration, template)`, not from the OAuth app row.

### Token storage layout

| Item id pattern | Content |
|---|---|
| `oauth:{owner}:{integration}:{name}` | Access token |
| `oauth:{owner}:{integration}:{name}:refresh` | Refresh token |
| `oauth-client:{owner}:{slug}:secret` | OAuth app client secret |

The connection row records `oauthClient`, `oauthClientOwner`, `oauthScope`, `expiresAt`, and optional regional `oauthTokenUrl` override (Datadog multi-site).

### Dynamic Client Registration

`POST /oauth/clients/register-dynamic` mints a public PKCE client via the server's `registration_endpoint`. The user supplies no client id/secret. Auth method negotiation: `none` when advertised or empty; otherwise `client_secret_post`.

### Probe

`POST /oauth/probe` with `{ url }` tries RFC 9728 protected-resource metadata first, then RFC 8414/OIDC authorization-server metadata. Returns `authorizationUrl`, `tokenUrl`, `scopesSupported`, `registrationEndpoint`, and related fields for UI pre-fill.

<Warning>
`redirectUri` has no localhost default. An executor constructed without it rejects redirect-requiring flows instead of registering a wrong callback URL with providers.
</Warning>

## Runtime resolution

On each tool invocation, the executor:

1. Loads the connection row and resolves the registered `CredentialProvider`.
2. For OAuth connections with expired (or near-expired) tokens, refreshes first (shared in-flight gate prevents parallel refresh races).
3. Fetches each `item_ids[variable]` from the provider.
4. Passes `values` to the protocol plugin, which renders placements onto the outbound request.

Errors surface as `CredentialProviderNotRegisteredError` (HTTP 409), `InvalidConnectionInputError` (HTTP 400), or `CredentialResolutionError` with `reauthRequired: true` when refresh fails.

## Configure from the web UI

The connect modal mirrors the HTTP payloads:

1. Pick owner (`org` / `user`), connection name, and auth method from the integration catalog.
2. For apikey methods, fill one field per `requiredPlacementVariables` entry (or browse external provider items via `GET /providers/:key/items`).
3. For OAuth, select an OAuth app, call `POST /oauth/start`, complete the browser redirect, and receive the minted connection.
4. Verify with `connections.list` or `tools.list` filtered by integration.

Provider choice is automatic: pasted values go to the default writable provider; `from` references route to the named external provider.

## Failure modes

| Symptom | Likely cause | Recovery |
|---|---|---|
| `Credential provider "default" is not registered` | No writable provider registered | Add `fileSecretsPlugin`, `keychainPlugin`, inline provider, or `workos-vault` |
| `Expected exactly one credential origin` | Multiple of `value`, `values`, `from` in one payload | Send exactly one origin field |
| `A connection cannot mix pasted and external-provider inputs` | Mixed `value` + `from` (or `inputs` mix) | Use all pasted or all external per connection |
| `OAuth redirect flow requires a configured redirectUri` | Host missing callback URL | Set `redirectUri` on `createExecutor`; local default is `{webBaseUrl}/api/oauth/callback` |
| `OAuth session expired or not found` | TTL exceeded or cancelled session | Restart with `POST /oauth/start` |
| Keychain provider missing | Probe failed at startup | Use `file` provider or fix secret-service on Linux |
| 1Password returns null | Not configured or URI outside vault | Run `onepassword.configure`; check vault scope |

## Related pages

<CardGroup>
<Card title="Connections" href="/connections">
Owner-scoped credential identity, provider resolution, and the `(owner, integration, name)` model.
</Card>

<Card title="Integrations" href="/integrations">
How auth method descriptors are declared and projected into the catalog.
</Card>

<Card title="HTTP API reference" href="/http-api-reference">
Full route listings for `connections`, `providers`, and `oauth` groups.
</Card>

<Card title="Embed with the SDK" href="/embed-sdk">
Compose `createExecutor` with plugins and credential providers in application code.
</Card>

<Card title="Plugin catalog" href="/plugin-catalog">
Published provider plugins and how `examples/all-plugins` wires them.
</Card>

<Card title="Configuration reference" href="/configuration-reference">
`EXECUTOR_DATA_DIR`, `EXECUTOR_WEB_BASE_URL`, and OAuth callback derivation.
</Card>
</CardGroup>
