Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
28 changes: 25 additions & 3 deletions lib/agentic/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) || {}

Expand Down Expand Up @@ -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]
)
Expand Down
25 changes: 22 additions & 3 deletions spec/agentic/cli/config_commands_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions spec/agentic/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading