Skip to Content

Credential Resolver

Every cmdop connect call (and every connecttool agent operation) needs a workspace credential. The credential resolver is the small state machine that turns the soup of CLI flags, env vars, and stored workspaces into one resolved key — and tells you which source it came from when something goes wrong.

This page covers the resolver’s precedence, how to debug a wrong resolution, and the design rationale for the order.

The precedence chain

workspace.Resolver.ResolveCtx() (in internal/connect/workspace/) walks six sources, top to bottom, and returns the first one that yields a usable key:

  1. --api-key flag — explicit per-call override on the CLI or tool argument.
  2. CMDOP_API_KEY env var — process-wide override.
  3. Named workspace--workspace=<name> selects a specific stored workspace, even if it is not the active one.
  4. Active workspace — whichever workspace is marked active in ~/.cmdop/ssh_workspaces.json.
  5. Legacycfg.Chat.GrpcAPIKey from older configs. One-shot migration into the default workspace on first open; no longer read after that.
  6. OAuth access token — from cmdop login. 72-hour lifespan with refresh.

The first source that returns a non-empty key wins. The result is tagged with a Source constant so error messages can tell you where the key came from:

const ( SourceFlag Source = "flag" SourceEnv Source = "env" SourceWorkspace Source = "workspace" SourceLegacy Source = "legacy" SourceOAuth Source = "oauth" )

The resolver also returns a tag for “no source found” — that surfaces as the auth_error class in server-to-server.

Why this order

Each step is more global than the last. The order is built so that a quick override always wins, and the most “passive” source (OAuth that you logged into long ago) is the fallback.

  • Flag beats everything. A one-off CI run should not depend on the operator’s config file.
  • Env beats stored. Containers and CI pass keys via env; that has to defeat whatever happens to be on disk.
  • Named beats active. If you say --workspace staging, the resolver should pick that workspace’s key even if production is active.
  • Legacy is a single migration window. It exists to import old configs once, then never again.
  • OAuth is the safety net. If you forgot to set up workspaces but ran cmdop login, you can still talk to your machines.

Concrete examples

Per-call override

cmdop connect --api-key ck_live_temp_xxx exec vps-audi -- uptime

The flag (Source 1) wins. Env, workspaces, and OAuth are skipped entirely for this call.

Container with env

docker run --rm \ -e CMDOP_API_KEY=ck_live_xxx \ cmdop:latest connect exec prod-api-1 -- uptime

The env var (Source 2) wins. The container has no ssh_workspaces.json and no OAuth state, but the env value carries the call.

Pinned workspace

cmdop connect --workspace staging exec vps-bmw -- uptime

Resolver.ResolveCtx() finds staging in the local store (Source 3) and returns its key. The active workspace is unchanged.

Active workspace

cmdop connect exec vps-audi -- uptime

No flag, no CMDOP_API_KEY env, no --workspace. The resolver falls to the active workspace (Source 4) and uses its key. This is the everyday case.

OAuth fallback

cmdop login # browser flow cmdop connect --list # no flag, no env, no stored workspace

If no API key is set anywhere, but you have a fresh OAuth token, Source 6 carries the call. The relay sees a different credential shape (Bearer token, not API key) but the resolver papers over the difference.

Debugging which source fired

The resolver tags every Resolved struct with the Source it picked. Errors include this tag:

auth_error: no API key for workspace "production" → tried: flag (none), env (none), workspace=production (no key), legacy (no value), oauth (no token)

For unstructured calls, the tag shows up in --json output:

$ cmdop connect --json --list { "credential": { "source": "workspace", "workspace": "production", "key_preview": "ck_live_***...***xa3b" }, "machines": [ ... ] }

MaskKey() (in internal/connect/workspace/types.go) is the helper that builds the masked preview. It always shows the first few and last few characters; the middle is fixed-width asterisks.

Per-call vs persistent config

A short cheat sheet on which knob to use when:

You wantUse
One-off override for a single command.--api-key flag.
Override for an entire shell session or CI job.CMDOP_API_KEY env.
Switch defaults for the next 100 commands.cmdop connect workspace use <name>.
Inject a key for a specific named workspace.cmdop connect key set <key> (after workspace use).
Forget the key and rely on OAuth instead.cmdop connect key clear and cmdop login.

Workspace --workspace vs activate

--workspace=<name> is per-call. It resolves to that workspace’s credentials without changing the active workspace. Two side-by-side calls with different --workspace values do not race or interfere.

cmdop connect workspace use <name> mutates the on-disk active pointer. The next daemon connection picks up the new workspace ID and reconnects. If your daemon is currently connected, this triggers a relay reconnect — see workspaces for the full note.

Server override

A workspace can carry a per-workspace gRPC Server override. When set, the resolver returns it alongside the key, and the dial uses that endpoint instead of the default cloud relay. Most users never touch this; on-prem deployments and test environments are the typical consumers.

What the resolver does not do

  • It does not fetch keys from the server. cmdop connect workspace sync does that, on demand.
  • It does not validate keys. A wrong key will resolve cleanly, carry to the relay, and fail there with unauthenticated.
  • It does not care which machine you are targeting. Workspace credentials gate the relay; per-machine identity is enforced separately.
  • It does not store anything. Reading is via DefaultStore(); writing is the explicit cmdop connect key set / workspace use paths.

When OAuth and an API key both exist

This combination is common: you ran cmdop login for the dashboard, and you also stored an API key for one workspace. The resolver prefers the API key (Source 4) over the OAuth token (Source 6).

Concretely: if your active workspace has a stored API key, that is what flows. If you cmdop connect key clear for that workspace, the next call falls through to OAuth.

This is the behavior you want — explicit local config beats whatever your dashboard session carries — but it surprises people who expect the dashboard’s identity to win.

A stored API key beats the OAuth fallback. To force the OAuth path, clear the key (cmdop connect key clear) or switch to a workspace that has no key set.

Sources, recapped

1. --api-key <flag> ← single call 2. CMDOP_API_KEY <env> ← process / container 3. --workspace=<name> ← single call, picks stored workspace 4. active workspace key ← persistent default 5. legacy cfg.Chat.GrpcAPIKey ← migration only, one-shot 6. OAuth from `cmdop login` ← safety net (none) → auth_error

Where stored credentials live and how to switch them.

The other layer — machine passwords and the session token cache.

Where --api-key, --workspace, and key set/get/clear are documented in CLI form.

auth_error is the resolver’s “no source” return path.

Last updated on