// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 #![cfg(feature = "e2e")] //! E2E test: `--provider ` auto-creates a provider from local credentials. //! //! When `--provider claude-code` is passed and no provider named "claude-code" exists, //! the CLI should discover `ANTHROPIC_API_KEY` from the local environment, //! auto-create a provider, and inject a supervisor-managed placeholder into the //! sandbox child process environment. //! //! The sandbox command (`printenv ANTHROPIC_API_KEY`) verifies that the //! placeholder made it all the way through to the sandbox process environment. //! //! Prerequisites: //! - A running openshell gateway (`mise run gateway:docker`) //! - The `openshell` binary (built automatically from the workspace) use std::process::Stdio; use std::sync::Mutex; use openshell_e2e::harness::binary::openshell_cmd; use openshell_e2e::harness::output::{extract_field, strip_ansi}; #[cfg(feature = "e2e-local-container-driver")] use openshell_e2e::harness::sandbox::E2E_WORKLOAD_IMAGE; const TEST_API_KEY: &str = "sk-e2e-auto-provider-test-key"; static CLAUDE_PROVIDER_LOCK: Mutex<()> = Mutex::new(()); fn contains_placeholder_for_env_key(output: &str, key: &str) -> bool { let legacy = format!("openshell:resolve:env:{key}"); let revision_prefix = "openshell:resolve:env:v"; let revision_suffix = format!("_{key}"); output.split_whitespace().any(|token| { token == legacy || (token.starts_with(revision_prefix) && token.ends_with(&revision_suffix)) }) } /// Helper: delete a provider by name, ignoring errors. async fn delete_provider(name: &str) { let mut cmd = openshell_cmd(); cmd.arg("provider") .arg("delete") .arg(name) .stdout(Stdio::null()) .stderr(Stdio::null()); let _ = cmd.status().await; } /// Helper: check whether a provider already exists. async fn provider_exists(name: &str) -> bool { let mut cmd = openshell_cmd(); cmd.arg("provider") .arg("get") .arg(name) .stdout(Stdio::null()) .stderr(Stdio::null()); cmd.status().await.is_ok_and(|status| status.success()) } /// Helper: delete a sandbox by name, ignoring errors. async fn delete_sandbox(name: &str) { let mut cmd = openshell_cmd(); cmd.arg("sandbox") .arg("delete") .arg(name) .stdout(Stdio::null()) .stderr(Stdio::null()); let _ = cmd.status().await; } /// `--provider claude-code --auto-providers` with `ANTHROPIC_API_KEY` set should /// auto-create a "claude-code" provider and inject a placeholder into the sandbox. #[tokio::test] async fn auto_created_provider_credential_available_in_sandbox() { let _provider_lock = CLAUDE_PROVIDER_LOCK .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if provider_exists("claude-code").await { eprintln!("Skipping test: existing provider 'claude-code' would make shared state unsafe"); return; } // Clean up any leftover from a previous run. delete_provider("claude-code").await; // This test only reads the injected environment placeholder. Do not inherit // the published image's network rules, which may be incompatible with the // attached credential provider's startup validation. let policy = tempfile::NamedTempFile::new().expect("create provider test policy"); std::fs::write( policy.path(), r"version: 1 filesystem_policy: include_workdir: true read_only: [/usr, /lib, /etc, /proc] read_write: [/sandbox, /tmp, /dev/null] landlock: compatibility: best_effort process: run_as_user: sandbox run_as_group: sandbox network_policies: {} ", ) .expect("write provider test policy"); // Create a sandbox that prints the ANTHROPIC_API_KEY env var. // --auto-providers skips the interactive prompt. let mut cmd = openshell_cmd(); cmd.arg("sandbox").arg("create"); #[cfg(feature = "e2e-local-container-driver")] cmd.arg("--from").arg(E2E_WORKLOAD_IMAGE); cmd.arg("--detach") .arg("--policy") .arg(policy.path()) .arg("--provider") .arg("claude-code") .arg("--auto-providers") .args(["--", "sh", "-c", "exec sleep infinity"]) .env("ANTHROPIC_API_KEY", TEST_API_KEY) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let output = cmd .output() .await .expect("failed to spawn openshell sandbox create"); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let combined = format!("{stdout}{stderr}"); let clean = strip_ansi(&combined); // Parse sandbox name for cleanup. let sandbox_name = extract_field(&combined, "Created sandbox"); let exec_output = if let Some(ref name) = sandbox_name { let mut exec_cmd = openshell_cmd(); exec_cmd .args([ "sandbox", "exec", "--name", name, "--no-tty", "--", "printenv", "ANTHROPIC_API_KEY", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); Some(exec_cmd.output().await.expect("failed to run sandbox exec")) } else { None }; // Always clean up, even if assertions fail. if let Some(ref name) = sandbox_name { delete_sandbox(name).await; } delete_provider("claude-code").await; // Now assert. assert!( output.status.success(), "sandbox create should succeed (exit {:?}):\n{clean}", output.status.code() ); let exec_output = exec_output.expect("sandbox name should be present"); let exec_clean = strip_ansi(&format!( "{}{}", String::from_utf8_lossy(&exec_output.stdout), String::from_utf8_lossy(&exec_output.stderr) )); assert!( exec_output.status.success(), "sandbox exec should succeed:\n{exec_clean}" ); assert!( clean.contains("Created provider claude-code"), "output should confirm provider auto-creation:\n{clean}" ); assert!( contains_placeholder_for_env_key(&exec_clean, "ANTHROPIC_API_KEY"), "sandbox should have placeholder ANTHROPIC_API_KEY in its environment:\n{exec_clean}" ); assert!( !exec_clean.contains(TEST_API_KEY), "sandbox should not expose the raw ANTHROPIC_API_KEY secret:\n{exec_clean}" ); }