diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 7c6f0ff50..c02e98c24 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -16,10 +16,29 @@ just wheels python -m pip install --find-links dist "nemo-fabric[claude]" ``` -Claude Code authentication can come from an existing cached login or from -`ANTHROPIC_API_KEY`. Package installation is verified by the adapter wheel and -module-entrypoint tests. Authentication is validated when Claude starts the -invocation. +## Authentication + +Fabric preserves Claude's native credential resolution. Use an existing Claude +Code login for local development, `ANTHROPIC_AUTH_TOKEN` for a gateway or proxy +bearer credential, `ANTHROPIC_API_KEY` for a static API credential, or Anthropic +Workload Identity Federation (WIF) for production and CI workloads that should +not store a long-lived API key. + +The adapter forwards the Anthropic profile and federation environment variables +that Claude Code and the Claude Agent SDK consume. This includes +`ANTHROPIC_CONFIG_DIR`, `ANTHROPIC_PROFILE`, the direct federation identifiers, +and `ANTHROPIC_IDENTITY_TOKEN` or `ANTHROPIC_IDENTITY_TOKEN_FILE`. Fabric reads +selected environment values and forwards them to the Claude runtime, but it +does not persist or log them in configuration or artifacts. Authentication is +validated when Claude starts the invocation. + +Unset unused `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` variables before +using WIF. Anthropic credential resolution treats an empty variable as selected, +so an empty API credential prevents fallback to a federation profile. + +Refer to the [Claude adapter authentication guide](https://nvidia-nemo-fabric.docs.buildwithfern.com/nemo/fabric/integrations/claude) +for mode selection, required WIF variables, and the Relay boundary. Package +installation is verified by the adapter wheel and module-entrypoint tests. Relay-enabled runs also require the external `nemo-relay` CLI. Install the CLI separately: diff --git a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py index 144639748..447ced738 100644 --- a/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py +++ b/adapters/claude/src/nemo_fabric_adapters/claude/adapter.py @@ -52,6 +52,15 @@ } INHERITED_ENV_NAMES = { "ANTHROPIC_API_KEY", + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_CONFIG_DIR", + "ANTHROPIC_FEDERATION_RULE_ID", + "ANTHROPIC_IDENTITY_TOKEN", + "ANTHROPIC_IDENTITY_TOKEN_FILE", + "ANTHROPIC_ORGANIZATION_ID", + "ANTHROPIC_PROFILE", + "ANTHROPIC_SERVICE_ACCOUNT_ID", + "ANTHROPIC_WORKSPACE_ID", "APPDATA", "CLAUDE_CONFIG_DIR", "COMSPEC", @@ -704,7 +713,7 @@ def child_environment( ) -> dict[str, str]: values = {name: "" for name in os.environ} values.update( - {name: value for name in INHERITED_ENV_NAMES if (value := os.environ.get(name))} + {name: os.environ[name] for name in INHERITED_ENV_NAMES if name in os.environ} ) model = _selected_model_config(payload) api_key_env = model.get("api_key_env") diff --git a/docs/index.yml b/docs/index.yml index 749c0b95a..a7fe9d94d 100644 --- a/docs/index.yml +++ b/docs/index.yml @@ -14,6 +14,8 @@ navigation: path: ./sdk/python.mdx - section: Integrations contents: + - page: Claude + path: ./integrations/claude.mdx - page: Harbor path: ./integrations/harbor.mdx - section: Reference diff --git a/docs/integrations/claude.mdx b/docs/integrations/claude.mdx new file mode 100644 index 000000000..00f82bc61 --- /dev/null +++ b/docs/integrations/claude.mdx @@ -0,0 +1,90 @@ +--- +title: "Authenticate the Claude Adapter" +description: "Configure cached login, bearer token, API key, or Workload Identity Federation authentication for the NeMo Fabric Claude adapter." +--- +{/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 */} + +# Authenticate the Claude Adapter + +The `nvidia.fabric.claude` adapter uses the Claude Agent SDK and its bundled +Claude Code runtime. Fabric preserves Claude's native credential precedence and +forwards only supported operating-system, configuration, and authentication +variables plus values explicitly configured in `harness.settings.env`. + +## Choose an Authentication Mode + +Use the mode that matches the execution environment: + +| Mode | Credential Source | Recommended Use | +| --- | --- | --- | +| Claude Code login | Cached Claude.ai or Anthropic Console login | Interactive local development | +| Bearer token | `ANTHROPIC_AUTH_TOKEN` | LLM gateways or proxies that authenticate with an `Authorization: Bearer` header | +| API key | `ANTHROPIC_API_KEY` | Local development, prototypes, and controlled servers | +| Workload Identity Federation (WIF) | Anthropic profile or direct federation environment variables | CI, cloud workloads, and Kubernetes without static API keys | + +Claude Code selects `ANTHROPIC_AUTH_TOKEN` before `ANTHROPIC_API_KEY`. Both +environment credentials take precedence over WIF and cached login credentials. +An empty value still occupies its precedence slot, so unset an unused variable +instead of setting it to an empty string. + +Claude Code can use a Claude.ai Pro or Max login, an Anthropic Console login, or +supported enterprise providers. Refer to the +[Claude Code authentication documentation](https://docs.anthropic.com/en/docs/claude-code/getting-started) +for the current subscription and enterprise options. + +## Use a Cached Claude Code Login + +Authenticate Claude Code outside Fabric. The adapter inherits `HOME` and +`CLAUDE_CONFIG_DIR`, so the bundled runtime can reuse the same cached login. +Fabric does not copy the credential store into its configuration or artifacts. + +## Use an API Key + +Set the API key in the process that invokes Fabric: + +```bash +export ANTHROPIC_API_KEY=sk-ant-api03-example +``` + +The adapter also forwards the selected model's `api_key_env` when the model +configuration names a different environment variable. + +## Use Workload Identity Federation + +For a named WIF profile, set the profile and optional nondefault configuration +directory: + +```bash +export ANTHROPIC_CONFIG_DIR=/etc/anthropic +export ANTHROPIC_PROFILE=production +``` + +For direct environment configuration, provide the federation rule, +organization, service account, and one identity-token source: + +```bash +export ANTHROPIC_FEDERATION_RULE_ID=fdrl_example +export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000 +export ANTHROPIC_SERVICE_ACCOUNT_ID=svac_example +export ANTHROPIC_WORKSPACE_ID=wrkspc_example +export ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic.com/token +``` + +Use `ANTHROPIC_IDENTITY_TOKEN` instead of +`ANTHROPIC_IDENTITY_TOKEN_FILE` when the platform injects the identity token as +an environment variable. Refer to the +[Anthropic WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference) +for credential precedence, profile structure, required claims, and provider +setup. + +Unset `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` before using WIF. These +variables take precedence over federation even when their value is empty. + +## Use Authentication with Relay + +NeMo Relay does not authenticate Claude. A Relay-enabled Fabric invocation +starts the gateway as a supervised sidecar and sets `ANTHROPIC_BASE_URL` for the +Claude runtime. Claude still resolves its credential through the selected mode, +and Fabric does not write authentication values to Relay configuration or +artifacts. diff --git a/tests/adapters/test_claude_adapter.py b/tests/adapters/test_claude_adapter.py index edfd9b0f5..2aa470da0 100644 --- a/tests/adapters/test_claude_adapter.py +++ b/tests/adapters/test_claude_adapter.py @@ -27,6 +27,18 @@ from nemo_fabric_adapters.claude import adapter ROOT = Path(__file__).resolve().parents[2] +ANTHROPIC_AUTH_ENV_NAMES = { + "ANTHROPIC_API_KEY", + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_CONFIG_DIR", + "ANTHROPIC_FEDERATION_RULE_ID", + "ANTHROPIC_IDENTITY_TOKEN", + "ANTHROPIC_IDENTITY_TOKEN_FILE", + "ANTHROPIC_ORGANIZATION_ID", + "ANTHROPIC_PROFILE", + "ANTHROPIC_SERVICE_ACCOUNT_ID", + "ANTHROPIC_WORKSPACE_ID", +} def test_claude_descriptor_is_narrow_and_versioned(): @@ -748,16 +760,60 @@ def test_run_reports_relay_start_failure_without_raw_diagnostic( assert not relay.plugin_path.exists() -def test_build_options_forwards_default_anthropic_api_key(claude_payload, monkeypatch): +@pytest.mark.parametrize( + "auth_environment", + [ + { + "ANTHROPIC_CONFIG_DIR": "/run/anthropic", + "ANTHROPIC_PROFILE": "production", + }, + { + "ANTHROPIC_FEDERATION_RULE_ID": "fdrl_test", + "ANTHROPIC_ORGANIZATION_ID": "organization-test", + "ANTHROPIC_SERVICE_ACCOUNT_ID": "svac_test", + "ANTHROPIC_WORKSPACE_ID": "wrkspc_test", + "ANTHROPIC_IDENTITY_TOKEN_FILE": "/run/secrets/anthropic/token", + }, + { + "ANTHROPIC_FEDERATION_RULE_ID": "fdrl_test", + "ANTHROPIC_ORGANIZATION_ID": "organization-test", + "ANTHROPIC_SERVICE_ACCOUNT_ID": "svac_test", + "ANTHROPIC_IDENTITY_TOKEN": "identity-token", + }, + {"ANTHROPIC_API_KEY": "default-secret"}, + {"ANTHROPIC_AUTH_TOKEN": "bearer-token"}, + { + "ANTHROPIC_API_KEY": "", + "ANTHROPIC_PROFILE": "fallback-profile", + }, + { + "ANTHROPIC_AUTH_TOKEN": "", + "ANTHROPIC_API_KEY": "fallback-api-key", + "ANTHROPIC_PROFILE": "fallback-profile", + }, + ], +) +def test_build_options_forwards_anthropic_auth_environment( + claude_payload, auth_environment +): model = claude_payload["effective_config"]["config"]["models"]["default"] model.pop("api_key_env") settings = claude_payload["effective_config"]["config"]["harness"]["settings"] settings.pop("env") - monkeypatch.setenv("ANTHROPIC_API_KEY", "default-secret") + for name in ANTHROPIC_AUTH_ENV_NAMES: + os.environ.pop(name, None) + os.environ["FABRIC_UNRELATED_SECRET"] = "do-not-forward" + os.environ.update(auth_environment) options = adapter.build_options(claude_payload, resume=None) - assert options.env["ANTHROPIC_API_KEY"] == "default-secret" + forwarded_auth_environment = { + name: options.env[name] + for name in ANTHROPIC_AUTH_ENV_NAMES + if name in options.env + } + assert forwarded_auth_environment == auth_environment + assert options.env["FABRIC_UNRELATED_SECRET"] == "" @pytest.mark.parametrize(