diff --git a/CHANGELOG.md b/CHANGELOG.md index a314b10..811daec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [Unreleased] +### Security +- The CLI no longer steers the OpenAI key into `.agentic.yml`: `agentic config set` refuses keys containing token, key, secret, passw, or credential in any spelling, and values shaped like an OpenAI key, and the missing-token error and README point at `OPENAI_ACCESS_TOKEN` / `Agentic.configure`, which are the paths that actually work. Nothing ever read `api_token` from that file, and it is not gitignored. + ### Fixed - Test suite loads on Ruby 4.0: `benchmark` is a bundled (not default) gem there, so it is now declared in the Gemfile - `EventDispatcher` slow-observer spec uses a monotonic clock and a looser bound so it no longer flakes on loaded hosts diff --git a/README.md b/README.md index e80ec88..caaaed7 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,7 @@ $ agentic config init # Initialize configuration $ agentic config init --global # Initialize global configuration $ agentic config list # List all configuration settings $ agentic config get model # Get a specific configuration value -$ agentic config set api_token=your_openai_api_key # Set your OpenAI API token -$ agentic config set model=gpt-4o-mini # Configure the default model +$ agentic config set model=gpt-4o-mini # Configure the default model # Plan Creation and Execution $ agentic plan "Generate a market research report on AI trends" # Create a plan @@ -729,20 +728,17 @@ You can configure the OpenAI API key in several ways: export OPENAI_ACCESS_TOKEN="your_api_key" ``` -2. Configuration file: - Create `.agentic.yml` in your project directory or home directory: - ```yaml - api_token: "your_api_key" - model: "gpt-4o-mini" - ``` - -3. In your Ruby code: +2. In your Ruby code: ```ruby Agentic.configure do |config| config.access_token = "your_api_key" end ``` +Keep the key out of `.agentic.yml`: that file holds non-secret settings such +as `model`, is written in plain text to your project directory, and is not read +for credentials. `agentic config set` refuses keys that look like secrets. + ### LLM Configuration You can customize the LLM behavior: diff --git a/lib/agentic/cli.rb b/lib/agentic/cli.rb index db18da2..01f1112 100644 --- a/lib/agentic/cli.rb +++ b/lib/agentic/cli.rb @@ -468,6 +468,15 @@ def get(key) end end + # Keys that look like credentials (api_token, apiKey, api-key, client_secret, + # db_password, credentials) and values shaped like an OpenAI key. + # .agentic.yml is plain text, is not gitignored, and nothing reads a + # token out of it, so storing one there is a leak with no payoff. The + # key check is a substring match on purpose: a false positive costs a + # rename, a false negative costs a secret + CREDENTIAL_KEY = /token|key|secret|passw|credential/i + CREDENTIAL_VALUE = /\Ask-[A-Za-z0-9_-]{16,}\z/ + desc "set KEY=VALUE", "Set a configuration setting" option :global, type: :boolean, aliases: "-g", desc: "Set in global user config instead of project config" @@ -479,6 +488,19 @@ def set(key_value) exit 1 end + if key.match?(CREDENTIAL_KEY) || value.match?(CREDENTIAL_VALUE) + puts UI.box( + "Refusing to store a credential", + "'#{key}' looks like a secret, and #{CONFIG_FILE_NAME} is a plain-text file " \ + "that nothing reads credentials from.\n\n" \ + "Set OPENAI_ACCESS_TOKEN in the environment instead, or use\n" \ + "Agentic.configure { |c| c.access_token = \"...\" } in code.", + padding: [1, 2, 1, 2], + style: {border: {fg: :red}} + ) + exit 1 + end + path = options[:global] ? USER_CONFIG_PATH : PROJECT_CONFIG_PATH config = load_config(path) || {} @@ -841,9 +863,9 @@ def check_api_token! "Configuration Error", "No OpenAI API token configured.\n\n" \ "You can set it using one of these methods:\n" \ - "- Environment variable: OPENAI_ACCESS_TOKEN\n" \ - "- Configuration file: .agentic.yml\n" \ - "- Run: agentic config set api_token=your_token", + "- Environment variable: OPENAI_ACCESS_TOKEN (or AGENTIC_API_TOKEN)\n" \ + "- In code: Agentic.configure { |c| c.access_token = \"...\" }\n" \ + "- Local endpoint: AGENTIC_API_BASE_URL, which needs no token", style: {border: {fg: :red}}, padding: [1, 2, 1, 2] ) diff --git a/spec/agentic/cli/config_commands_spec.rb b/spec/agentic/cli/config_commands_spec.rb index b89c026..3321b1e 100644 --- a/spec/agentic/cli/config_commands_spec.rb +++ b/spec/agentic/cli/config_commands_spec.rb @@ -99,11 +99,11 @@ end it "sets a configuration value" do - config_commands.set("test_key=test_value") + config_commands.set("test_option=test_value") expect(output.string).to include("Configuration Updated:") - expect(output.string).to match(/Set .*test_key.* to .*test_value.*/) - expect(config_commands).to have_received(:save_config).with(config_path, {"test_key" => "test_value"}) + expect(output.string).to match(/Set .*test_option.* to .*test_value.*/) + expect(config_commands).to have_received(:save_config).with(config_path, {"test_option" => "test_value"}) end it "converts values to appropriate types" do @@ -134,6 +134,25 @@ expect(config_commands).to have_received(:save_config).with(config_path, {"test_string" => "hello"}) end + it "refuses credential-shaped keys and values and writes nothing" do + %w[ + api_token=sk-123 OPENAI_KEY=abc client_secret=x db_password=y + apiKey=abc api-key=abc apikey=abc accessToken=abc credentials=abc passwd=abc + model=sk-proj-abcdefghijklmnopqrstuvwxyz + ].each do |key_value| + expect { config_commands.set(key_value) }.to raise_error(SystemExit), "expected #{key_value} to be refused" + end + + expect(config_commands).not_to have_received(:save_config) + expect(output.string).to include("Refusing to store a credential") + expect(output.string).to include("OPENAI_ACCESS_TOKEN") + end + + it "still sets non-secret keys" do + config_commands.set("model=gpt-4o-mini") + expect(config_commands).to have_received(:save_config).with(config_path, {"model" => "gpt-4o-mini"}) + end + it "shows an error for invalid format" do # Instead of testing with the real implementation, mock the behavior diff --git a/spec/agentic/cli_spec.rb b/spec/agentic/cli_spec.rb index 99624e9..4bd985a 100644 --- a/spec/agentic/cli_spec.rb +++ b/spec/agentic/cli_spec.rb @@ -27,6 +27,20 @@ end end + describe "#check_api_token!" do + it "names the configuration paths that work, not .agentic.yml" do + allow(Agentic).to receive(:configuration).and_return( + instance_double(Agentic::LegacyConfiguration, access_token: nil, api_base_url: nil) + ) + allow(Agentic::UI).to receive(:box) { |title, content, _| "#{title}:\n#{content}" } + + expect { described_class.new.send(:check_api_token!) }.to raise_error(Thor::Error) do |error| + expect(error.message).to include("OPENAI_ACCESS_TOKEN", "Agentic.configure") + expect(error.message).not_to include(".agentic.yml", "config set api_token") + end + end + end + describe "agent commands" do describe "agent list" do it "displays available agents" do