Skip to content

Commit 9d9dc4f

Browse files
authored
Merge pull request #146 from editor-code-assistant/netrc
Add support for secrets stored in authinfo and netrc files
2 parents 499bdd8 + e0fddc8 commit 9d9dc4f

File tree

13 files changed

+1464
-33
lines changed

13 files changed

+1464
-33
lines changed

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,18 @@ Code Style
2424
- Unit tests should have a single `deftest` for function to be tested with multiple `testing`s for each tested case.
2525
- Unit tests that use file paths and uris should rely on `h/file-path` and `h/file-uri` to avoid windows issues with slashes.
2626

27+
Secrets Management
28+
- `src/eca/secrets/netrc.clj` - Netrc format parser (multi-line format: machine/login/password/port keywords)
29+
- `src/eca/secrets/authinfo.clj` - Authinfo format parser (single-line format: space-separated key-value pairs)
30+
- `src/eca/secrets.clj` - Main secrets manager for credential file operations:
31+
- File discovery and priority order (.authinfo.gpg → .netrc.gpg → .authinfo → _authinfo → .netrc → _netrc)
32+
- Cross-platform path construction using io/file (handles / vs \ separators automatically)
33+
- GPG decryption with caching (5-second TTL) and timeout (30s, configurable via GPG_TIMEOUT env var)
34+
- keyRc format parsing: [login@]machine[:port] (named after Unix "rc" config file tradition)
35+
- Credential matching logic (exact login match when specified, first match otherwise)
36+
- Permission validation (Unix: warns if not 0600; Windows: skipped)
37+
- Authentication flow: config `key` → credential files `keyRc` → env var `keyEnv` → OAuth
38+
- Security: passwords never logged; GPG decryption via clojure.java.process; cache with short TTL; subprocess timeout protection
39+
2740
Notes
2841
- CI runs: bb test and bb integration-test. Ensure these pass locally before PRs.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Add support for secrets stored in authinfo and netrc files
56
- Added tests for stopping concurrent tool calls. #147
67
- Improve logging.
78

deps-lock.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
"git-dir": "https/github.com/borkdude/gh-release-artifact",
99
"hash": "sha256-HllBWtwTtYYM0KjHB9UVDBjB57CtpvF+gncQseGXgf8="
1010
},
11+
{
12+
"lib": "br.dev.zz/parc",
13+
"url": "https://github.com/souenzzo/parc",
14+
"rev": "76f0714e6b93a053d67d473c33dfc279dfb94d23",
15+
"git-dir": "https/github.com/souenzzo/parc",
16+
"hash": "sha256-6ISYa9g5kOGkay3ums4Tys2/krPTIFqQKAtT8rfaZ4k="
17+
},
1118
{
1219
"lib": "io.github.clojure/tools.build",
1320
"url": "https://github.com/clojure/tools.build.git",

deps.edn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
borkdude/dynaload {:mvn/version "0.3.5"}
88
babashka/fs {:mvn/version "0.5.26"}
99
babashka/process {:mvn/version "0.6.23"}
10+
br.dev.zz/parc {:git/url "https://github.com/souenzzo/parc"
11+
:git/sha "76f0714e6b93a053d67d473c33dfc279dfb94d23"}
1012
com.cognitect/transit-clj {:mvn/version "1.0.333"}
1113
hato/hato {:mvn/version "1.0.0"}
1214
ring/ring-codec {:mvn/version "1.3.0"}

docs/configuration.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ For MCP servers configuration, use the `mcpServers` config, examples:
6868
"command": "npx",
6969
"args": ["-y", "@modelcontextprotocol/server-memory"],
7070
// optional
71-
"env": {"FOO": "bar"}
71+
"env": {"FOO": "bar"}
7272
}
7373
}
7474
}
7575
```
76-
76+
7777
=== "HTTP-streamable"
7878

7979
`~/.config/eca/config.json`
@@ -138,7 +138,7 @@ Check some examples:
138138
}
139139
}
140140
```
141-
141+
142142
=== "Matching by a tool argument"
143143

144144
__`argsMatchers`__ is a map of argument name by list of [java regex](https://www.regexplanet.com/advanced/java/index.html).
@@ -156,7 +156,7 @@ Check some examples:
156156
}
157157
}
158158
```
159-
159+
160160
=== "Denying a tool"
161161

162162
```javascript
@@ -336,7 +336,7 @@ There are 3 possible ways to configure rules following this order of priority:
336336
"rules": [{"path": "my-rule.md"}]
337337
}
338338
```
339-
339+
340340
## Behaviors / prompts
341341

342342
ECA allows to totally customize the prompt sent to LLM via the `behavior` config, allowing to have multiple behaviors for different tasks or workflows.
@@ -379,6 +379,7 @@ To configure, add your OTLP collector config via `:otlp` map following [otlp aut
379379
urlEnv?: string;
380380
key?: string; // when provider supports api key.
381381
keyEnv?: string;
382+
keyRc?: string; // credential file lookup in format [login@]machine[:port]
382383
completionUrlRelativePath?: string;
383384
models: {[key: string]: {
384385
extraPayload?: {[key: string]: any}

docs/models.md

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ Example:
3232
"providers": {
3333
"openai": {
3434
"key": "your-openai-key-here", // configuring a key
35-
"models": {
35+
"models": {
3636
"o1": {} // adding models to a built-in provider
3737
"o3": {
3838
"extraPayload": { // adding to the payload sent to LLM
3939
"temperature": 0.5
4040
}
4141
}
4242
}
43-
}
43+
}
4444
}
4545
}
4646
```
@@ -68,6 +68,7 @@ Schema:
6868
| `urlEnv` | string | Environment variable name containing the API URL | No* |
6969
| `url` | string | Direct API URL (use instead of `urlEnv`) | No* |
7070
| `keyEnv` | string | Environment variable name containing the API key | No* |
71+
| `keyRc` | string | Lookup specification to read the API key from Unix RC [credential files](#credential-file-authentication) | No* |
7172
| `key` | string | Direct API key (use instead of `keyEnv`) | No* |
7273
| `completionUrlRelativePath` | string | Optional override for the completion endpoint path (see defaults below and examples like Azure) | No |
7374
| `models` | map | Key: model name, value: its config | Yes |
@@ -121,27 +122,54 @@ Defaults by API type:
121122

122123
Only set this when your provider uses a different path or expects query parameters at the endpoint (e.g., Azure API versioning).
123124

125+
### Credential File Authentication
126+
127+
Use `keyRc` in your provider config to read credentials from `~/.authinfo(.gpg)` or `~/.netrc(.gpg)` without storing keys directly in config or env vars.
128+
129+
Example:
130+
131+
```javascript
132+
{
133+
"providers": {
134+
"openai": {"keyRc": "api.openai.com"},
135+
"anthropic": {"keyRc": "[email protected]"}
136+
}
137+
}
138+
```
139+
140+
keyRc lookup specification format: `[login@]machine[:port]` (e.g., `api.openai.com`, `[email protected]`, `api.custom.com:8443`).
141+
142+
Further reading on credential file formats:
143+
- [Emacs authinfo documentation](https://www.gnu.org/software/emacs/manual/html_node/auth/Help-for-users.html)
144+
- [Curl Netrc documentation](https://everything.curl.dev/usingcurl/netrc)
145+
- [GNU Inetutils .netrc documentation](https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html)
146+
147+
Notes:
148+
- Preferred files are GPG-encrypted (`~/.authinfo.gpg` / `~/.netrc.gpg`); plaintext variants are supported.
149+
- Authentication priority (short): `key` > `keyRc files` > `keyEnv` > OAuth.
150+
- All providers with API key auth can use credential files.
151+
124152
## Providers examples
125153

126154
=== "Anthropic"
127-
155+
128156
1. Login to Anthropic via the chat command `/login`.
129157
2. Type 'anthropic' and send it.
130158
3. Type the chosen method
131159
4. Authenticate in your browser, copy the code.
132160
5. Paste and send the code and done!
133161

134162
=== "Github Copilot"
135-
163+
136164
1. Login to Github copilot via the chat command `/login`.
137165
2. Type 'github-copilot' and send it.
138166
3. Authenticate in Github in your browser with the given code.
139167
4. Type anything in the chat to continue and done!
140-
168+
141169
_Tip: check [Your Copilot plan](https://github.com/settings/copilot/features) to enable models to your account._
142-
170+
143171
=== "Google / Gemini"
144-
172+
145173
1. Login to Google via the chat command `/login`.
146174
2. Type 'google' and send it.
147175
3. Choose 'manual' and type your Google/Gemini API key. (You need to create a key in [google studio](https://aistudio.google.com/api-keys))
@@ -165,7 +193,7 @@ Only set this when your provider uses a different path or expects query paramete
165193
```
166194

167195
=== "OpenRouter"
168-
196+
169197
[OpenRouter](https://openrouter.ai) provides access to many models through a unified API:
170198

171199
1. Login via the chat command `/login`.
@@ -175,7 +203,7 @@ Only set this when your provider uses a different path or expects query paramete
175203
5. Done, it should be saved to your global config.
176204

177205
or manually via config:
178-
206+
179207
```javascript
180208
{
181209
"providers": {
@@ -196,15 +224,15 @@ Only set this when your provider uses a different path or expects query paramete
196224
=== "DeepSeek"
197225

198226
[DeepSeek](https://deepseek.com) offers powerful reasoning and coding models:
199-
227+
200228
1. Login via the chat command `/login`.
201229
2. Type 'deepseek' and send it.
202230
3. Specify your Deepseek API key.
203231
4. Inform at least a model, ex: `deepseek-chat`
204232
5. Done, it should be saved to your global config.
205-
233+
206234
or manually via config:
207-
235+
208236
```javascript
209237
{
210238
"providers": {
@@ -215,7 +243,7 @@ Only set this when your provider uses a different path or expects query paramete
215243
"models": {
216244
"deepseek-chat": {},
217245
"deepseek-coder": {},
218-
"deepseek-reasoner": {}
246+
"deepseek-reasoner": {}
219247
}
220248
}
221249
}
@@ -230,7 +258,7 @@ Only set this when your provider uses a different path or expects query paramete
230258
4. Specify your API url with your resource, ex: 'https://your-resource-name.openai.azure.com'.
231259
5. Inform at least a model, ex: `gpt-5`
232260
6. Done, it should be saved to your global config.
233-
261+
234262
or manually via config:
235263

236264
```javascript
@@ -256,7 +284,7 @@ Only set this when your provider uses a different path or expects query paramete
256284
3. Specify your API key.
257285
4. Inform at least a model, ex: `GLM-4.5`
258286
5. Done, it should be saved to your global config.
259-
287+
260288
or manually via config:
261289

262290
```javascript
@@ -278,7 +306,7 @@ Only set this when your provider uses a different path or expects query paramete
278306
=== "Same model with different settings"
279307

280308
For now, you can create different providers with same model names to achieve that:
281-
309+
282310
```javascript
283311
{
284312
"providers": {

src/eca/features/commands.clj

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
[eca.features.tools.mcp :as f.mcp]
1212
[eca.llm-api :as llm-api]
1313
[eca.messenger :as messenger]
14+
[eca.secrets :as secrets]
1415
[eca.shared :as shared :refer [multi-str update-some]])
1516
(:import
1617
[java.lang ProcessHandle]))
@@ -133,7 +134,9 @@
133134
(map-indexed vector args)))))
134135

135136
(defn ^:private doctor-msg [db config]
136-
(let [model (llm-api/default-model db config)]
137+
(let [model (llm-api/default-model db config)
138+
cred-check (secrets/check-credential-files)
139+
existing-files (filter :exists (:files cred-check))]
137140
(multi-str (str "ECA version: " (config/eca-version))
138141
""
139142
(str "Server cmd: " (.orElse (.commandLine (.info (ProcessHandle/current))) nil))
@@ -158,7 +161,26 @@
158161
(str s key "=" val "\n")
159162
s))
160163
"\n"
161-
(System/getenv))))))
164+
(System/getenv)))
165+
""
166+
(if (seq existing-files)
167+
(str "Credential files (GPG available: " (:gpg-available cred-check) "):"
168+
(reduce
169+
(fn [s file-info]
170+
(str s "\n " (:path file-info) ":"
171+
(when (contains? file-info :readable)
172+
(str "\n Readable: " (:readable file-info)))
173+
(when (contains? file-info :permissions-secure)
174+
(str "\n Permissions: " (if (:permissions-secure file-info) "secure" "INSECURE (should be 0600)")))
175+
(when (:credentials-count file-info)
176+
(str "\n Credentials: " (:credentials-count file-info)))
177+
(when (:parse-error file-info)
178+
(str "\n Parse error: " (:parse-error file-info)))
179+
(when (:suggestion file-info)
180+
(str "\n " (:suggestion file-info)))))
181+
""
182+
existing-files))
183+
(str "Credential files: None found (GPG available: " (:gpg-available cred-check) ")")))))
162184

163185
(defn handle-command! [command args {:keys [chat-id db* config messenger full-model instructions metrics]}]
164186
(let [db @db*

src/eca/llm_util.clj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
[cheshire.core :as json]
44
[clojure.string :as string]
55
[eca.config :as config]
6-
[eca.logger :as logger])
6+
[eca.logger :as logger]
7+
[eca.secrets :as secrets])
78
(:import
89
[java.io BufferedReader]
910
[java.nio.charset StandardCharsets]
@@ -96,7 +97,10 @@
9697
(defn provider-api-key [provider provider-auth config]
9798
(or (get-in config [:providers (name provider) :key])
9899
(:api-key provider-auth)
99-
(some-> (get-in config [:providers (name provider) :keyEnv]) config/get-env)))
100+
(when-let [key-rc (get-in config [:providers (name provider) :keyRc])]
101+
(secrets/get-credential key-rc))
102+
(some-> (get-in config [:providers (name provider) :keyEnv])
103+
config/get-env)))
100104

101105
(defn provider-api-url [provider config]
102106
(or (get-in config [:providers (name provider) :url])

0 commit comments

Comments
 (0)