Skip to content

Agents CLI: run against a remote Dex server with TLS and an API key or bearer credential - #1752

Merged
longquanzheng merged 8 commits into
mainfrom
agents-cli-remote-server-auth
Nov 13, 2026
Merged

longquanzheng merged 8 commits into
mainfrom
agents-cli-remote-server-auth

Conversation

@longquanzheng

@longquanzheng longquanzheng commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The agents CLI commands (Python and TypeScript) can now keep their sessions on a remote Dex server that requires TLS and a credential. Before, a command could name an https server but sent no credential and could not trust a private CA.

  • Configuration. Two optional sections join the configuration file: dex.auth and dex.tls.

    • dex.auth takes one of three modes: none (the default), api_key (with token_url and server_id), or bearer (with source: env or source: file).
    • dex.tls takes ca_file and server_name.
    • The secret never goes in the file. It comes from DEX_API_KEY, DEX_CREDENTIAL, or a token file. The schema rejects api_key, token, credential, and access_token under dex.auth.
  • Environment variables. They use dexcli's names: DEX_TOKEN_URL, DEX_SERVER_ID, DEX_CREDENTIAL_FILE, DEX_TLS_CA_FILE, and DEX_TLS_SERVER_NAME.

    • A variable that is set (after trimming) overrides the matching field of the mode the file selects.
    • The TLS variables are read only for an https dex.server.
  • New file checks.

    • insecure_token_url: the token URL must be https, or http on loopback.
    • auth_needs_server_url: dex.auth with dex.server: auto is an error.
    • tls_needs_https_server: dex.tls needs an https server.
  • Connections. Every Dex connection of a command uses the resolved credential and TLS options:

    • the Worker;
    • the session Client;
    • the sessions listing Client;
    • the Worker host of the terminal UI. It gets the settings without secrets in its start message and reads the secret from its own environment.
  • Terminal UI. The launch document carries the resolved dex.auth and dex.tls, which were hardcoded to none and defaults before. The terminal UI environment gets only the one credential variable that the contract allows. For bearer with source: file, that is DEX_CREDENTIAL_FILE set to the resolved path.

  • Errors.

    • A missing or invalid setting stops the command at startup with exit code 2 and a message that names the setting, such as DEX_API_KEY is not set; dex.auth mode api_key needs it.
    • A rejected credential stops it with exit code 1. The line says whether the token endpoint refused the key (could not exchange DEX_API_KEY for an access token at <token URL> (HTTP status 401)) or the server answered UNAUTHENTICATED or PERMISSION_DENIED.
    • The line names only the HTTP status or the gRPC status code. It leaves out the error text that the token endpoint or the server sent, because that text may echo the credential. No message holds a key or a token.
  • Child processes never get the credential. Each command removes DEX_API_KEY, DEX_CREDENTIAL, and DEX_CREDENTIAL_FILE from its own environment right after it reads them. It does so whether or not dex.auth is set, because a shell exported for dexcli may hold them. So the shell tool, the browser and its installer, the URL opener, downloads, and dexcli dev never see them, and an agent cannot read them with printenv.

    • The Worker host gets only the one variable that its mode needs.
    • dexcli agents-tui keeps the contract's one-variable allow-list.
    • TypeScript CliEnvironment.childProcessVariables lacks the three variables too.
  • connectors validates the settings in both cores. Python now resolves the connection settings before the connectors subcommand, as TypeScript does, so a missing DEX_API_KEY stops it with exit code 2 in both.

  • Shared contract. agents/conformance/cli_config.json gains:

    • the new file cases;
    • a dex_connection section, which fixes how the file and the variables become connection settings, with exact error messages.

    Both cores and the test-only reference resolver in agents/cli/tests run these cases.

Why

A user wants to run a command such as dex-agents-claude-sdk with its sessions on a shared remote Dex server. Such a server requires TLS and an API key or bearer credential. Several pieces already supported this:

  • the Dex SDKs, with ApiKeyCredential, FileCredential, and the FlowService TLS options;
  • dexcli, with the DEX_* variables;
  • terminal UI contract 1, whose launch document has dex.auth and dex.tls.

The CLI cores never passed a credential, and both launchers hardcoded {"mode":"none"}. So such a server rejected every call with UNAUTHENTICATED. The Go example examples/go/cmd/ai-agent (#1735) shows the same validation rules for an application.

Design decisions

  • Only the file turns authentication on. dex.auth.mode comes from the configuration file alone. Without it, the command reads none of the credential variables. A user who exports dexcli's variables for another server keeps a working auto or http configuration.
  • Variables override the file. A set variable overrides the matching field of the selected mode, as dexcli's variables do. One file can then point at another token endpoint or server ID per shell.
  • The URL scheme decides TLS. DEX_TLS and DEX_FLOW_SERVICE_ADDRESS are never read.
  • Plaintext is allowed. A credential may go to an http dex.server, as in dexcli and the SDKs. The docs say it is then readable on the network path. The token URL, in contrast, must be https or on loopback.
  • Paths. In the file, paths expand ~ and are relative to the configuration file's directory. In a variable, they expand ~ and are relative to the working directory. Both are normalized lexically.
  • Python checks the credential first. With auth on, the Python core makes one cheap Client call before it starts the Worker, because the Python pull Worker retries a failed token exchange until its 30-second registration timeout. The TypeScript Worker fails fast.

Compatibility

All changes are additive:

  • An existing configuration resolves to dex.auth: {mode: none} and behaves as before.
  • TypeScript EffectiveConfig.dex.auth and .tls are optional, so code that builds an EffectiveConfig itself keeps compiling.
  • Python's new dataclass fields have defaults.
  • The effective configuration JSON, and so the conformance file's expected values, now carry dex.auth and dex.tls.
  • The launch document still follows terminal UI contract 1. Only the values of the existing dex.auth and dex.tls fields change.
  • New API is marked Since 0.6.2, the next agents patch release.

Security notes

  • Secrets come only from the environment or a token file. The schema rejects secret-looking keys in dex.auth.

  • Startup and runtime messages name settings and variables, never their values. Tests assert that the output does not contain the key or the token.

  • The terminal UI environment stays an allow-list. It gets only the one auth variable the mode needs, and never provider keys.

  • The launch document holds no secret.

  • Child processes other than the Worker host and dexcli agents-tui never inherit DEX_API_KEY, DEX_CREDENTIAL, or DEX_CREDENTIAL_FILE. Tests in both languages run env through the shell tool, with and without dex.auth, and find none of them.

  • Refusal lines never repeat remote error details. Tests in both languages give the description a status whose details contain a secret.

  • The CLI cores' own log lines and fallback output name only the status code of an UNAUTHENTICATED or PERMISSION_DENIED answer. This covers the session graph upload log line, the Worker host's local request and start_session logs, the TypeScript plain chat, and the last-resort error print. Tests in both languages cover an upload refused with echoed details.

  • Log records and warnings that the Dex SDKs write, such as the Worker's automatic Flow definition upload failure, pass through the same rewrite:

    • in Python, a logging filter on the command's log handler, so every logger is covered;
    • in TypeScript, the library console.warn and console.error redirection.

    They cover any SDK log, pull stream reconnects included, that passes the error object itself. Follow-up: an SDK log that has already formatted the error into its message text, or that writes to stderr directly (TypeScript dex info: lines), is not rewritten. The SDKs should leave refusal details out themselves.

  • The token URL must be https, or http on loopback, as the SDKs require.

Testing

  • agents/cli/tests/ci.sh: schemas, the new example, and every config and dex_connection case against the reference resolver.
  • agents/cli/python/core/ci.sh with DEXCLI set to dexcli 1.46.1 and its dex-server-zd 0.1.26: ruff, format, mypy, and pytest, 714 passed and 2 skipped. New integration tests run against an authenticated dex-server-zd dev (staticTokens), a token endpoint that accepts only the right key, and a TLS proxy with a private CA:
    • api_key over TLS with dex.tls.ca_file: a chat turn completes, and sessions lists the session;
    • bearer env over plaintext;
    • missing DEX_API_KEY (exit 2, exact message);
    • wrong API key, and wrong bearer token (exit 1, no secret in the output);
    • the terminal UI launch document and environment;
    • a shell command run through the shell tool sees no Dex credential variable, with exported sentinels and no dex.auth, and with bearer auth;
    • connectors without DEX_API_KEY exits 2;
    • refusal lines leave out server details that echo a secret.
  • agents/cli/typescript/core/ci.sh with the same dexcli and dex-server-zd: typecheck, conformance, and integration tests, 438 passed and 0 failed. The new tests in test/integ/remote-dex-server.integration.test.ts use the same kind of fixture:
    • an API key over TLS with a private CA: a chat turn completes, and sessions lists the session;
    • a bearer token from DEX_CREDENTIAL over plaintext loopback;
    • a missing DEX_API_KEY (exit 2);
    • a refused key, and a refused bearer token (exit 1, no secret in the output);
    • the terminal UI launch document and environment, for api_key and bearer file;
    • a shell command sees no Dex credential variable, without dex.auth (in local-tools.integration.test.ts) and with bearer auth;
    • refusal lines leave out server details and a token exchange text that a server sends as details.
  • The TypeScript test nothing the Worker host, its children, the launcher's JavaScript output, or its opener prints reaches the terminal UI's screen is flaky on main too, so this PR does not cause it.
    • The test's own scripted harness prints import noise later every 500 ms. After the handoff, the Worker host forwards each line as a warning notice.
    • When one arrives between the open_url request and its response, the fake terminal UI reports a mismatch. A variant also mismatches the launch document's notices.
    • Run alone in a minimal environment (env -i HOME PATH DEXCLI DEX_SERVER_ZD_BINARY), the base commit of this branch failed 9 of 16 runs, and this branch failed 3 of 27. With the usual environment, it passed every run on this branch.
  • TypeScript SDK 1.45.0 does not export ApiKeyExchangeError. A refused exchange surfaces as UNAUTHENTICATED with details that start with dex: token exchange, so the TypeScript core recognizes the exchange failure by that prefix.
  • npm run typecheck passes for agents/cli/typescript/builtin against the changed core. agents/cli/typescript/claude-sdk shows 8 type errors in its adapter runner types (ReauthorizingClaudeAgentRunner). None involves the CLI core, and this PR does not touch them.
  • cd docs && npm run check passes.

Not tested: a real dexcli agents-tui session connecting with a credential, which the launch document and environment tests only approach; the PERMISSION_DENIED branch; unreadable CA or token files; and DEX_TLS_SERVER_NAME end to end. bearer file, no auth against an authenticated server, and auth with auto have conformance cases but no separate integration test.

…ormance cases

Add dex.auth (none, api_key, bearer) and dex.tls (ca_file, server_name)
to the CLI configuration schema, with the file checks
insecure_token_url, auth_needs_server_url, and tls_needs_https_server.
Add the dex_connection conformance cases that fix how a CLI core turns
the file and the DEX_* variables into connection settings, and check
them with the reference resolver.
Document dex.auth and dex.tls in the agents CLI reference, with the
environment variables, their precedence, and the startup errors, and add
the agents and agents-packages change-log fragments.
… dex.tls

Resolve dex.auth and dex.tls with the DEX_* variables, and give every
Client, the Worker, and the Worker host the same credential and TLS
options. Fill the terminal UI launch document's dex.auth and dex.tls and
pass it only the one credential variable that the mode needs. A missing
setting stops the command with a message that names it, and a rejected
credential says whether the token endpoint or the server refused it.
Integration tests run against an authenticated dex-server-zd, with a
token endpoint and a TLS proxy.
… and dex.tls

Resolve dex.auth and dex.tls with the DEX_* variables as the Python core
does, and give the Worker, every Client, and the Worker host the same
credential and TLS options. Fill the terminal UI launch document's
dex.auth and dex.tls and pass it only the one credential variable that
the mode needs. Run the dex_connection conformance cases, and add
integration tests against an authenticated dex-server-zd with a token
endpoint and a TLS proxy.
…r texts

Remove DEX_API_KEY, DEX_CREDENTIAL, and DEX_CREDENTIAL_FILE from the
environment of shell commands, browsers, downloads, and dexcli dev in
both cores, also without dex.auth; only the Worker host and the terminal
UI get the one variable that the mode needs. Refusal lines name only the
HTTP or gRPC status, never the remote error text, which may echo the
credential. The Python connectors subcommand validates the connection
settings as the TypeScript one does. Mark the new API as 0.6.2.
…ver-auth

# Conflicts:
#	agents/cli/typescript/core/src/cli-config.ts
The session graph upload log line, the Worker host's local request logs,
and the fallback error output name only the status of an UNAUTHENTICATED
or PERMISSION_DENIED answer, never the server's details, which may echo
the credential. The docs say that messages never show credential values;
the token URL they show is a configuration value.
…arnings

A logging filter on the Python CLI's log handler, and the TypeScript
library console redirection, replace an UNAUTHENTICATED or
PERMISSION_DENIED error that the SDK logs, such as a failed Worker Flow
definition upload, with its status code and a local hint.
@longquanzheng
longquanzheng enabled auto-merge (squash) November 13, 2026 02:17
@longquanzheng
longquanzheng merged commit 2087c9e into main Nov 13, 2026
55 checks passed
@longquanzheng
longquanzheng deleted the agents-cli-remote-server-auth branch November 13, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant