amicus 4.2.0 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/utils/api-key-store.js +7 -4
- package/src/utils/env-loader.js +0 -1
- package/src/utils/env-raw-store.js +13 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christian Wagner"
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to Amicus are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.2.1] - 2026-07-23
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **`.env` key writes strip CR/LF and re-assert `0600`.** A provider key or local-provider
|
|
13
|
+
bearer that carries a stray newline — a trailing `\r\n` from a paste, or an embedded one — is
|
|
14
|
+
now sanitized before it is persisted, so it can no longer split `~/.config/amicus/.env` into a
|
|
15
|
+
broken line or silently truncate the stored token; the sanitized value is also what lands in
|
|
16
|
+
the process environment. Every rewrite of the secrets file additionally re-asserts `0600`
|
|
17
|
+
permissions, re-tightening an existing `.env` whose mode had drifted (created under a loose
|
|
18
|
+
umask, or hand-edited). POSIX; a no-op on Windows/NTFS, where key-file ACL hardening remains a
|
|
19
|
+
separate tracked item.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Test-suite hardening: a global hermetic baseline pins the config and keys directories to a
|
|
24
|
+
per-worker scratch dir so no unit test can read or write the real `~/.config/amicus`. Scoped to
|
|
25
|
+
the unit run; the integration rails continue to manage their own credentials. This closes the
|
|
26
|
+
config/keys-dir door only — the OpenCode `auth.json` door is handled separately by the keyless
|
|
27
|
+
integration rail.
|
|
28
|
+
- Removed a redundant `.env` re-read in the credential loader, and cleaned up test scratch
|
|
29
|
+
directories that were leaking across runs.
|
|
30
|
+
|
|
8
31
|
## [4.2.0] - 2026-07-23
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Hand Claude a plan, a design, a diff, an architecture decision, a manuscript —
|
|
|
15
15
|
[](https://nodejs.org)
|
|
16
16
|
[](./CONTRIBUTING.md)
|
|
17
17
|
|
|
18
|
-
> **Supported clients:** Claude Code CLI and Claude Cowork are fully tested and supported. Claude Code web
|
|
18
|
+
> **Supported clients:** Claude Code CLI, Claude Desktop, and Claude Cowork are fully tested and supported. Claude Code web is experimental.
|
|
19
19
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
@@ -348,7 +348,7 @@ $ amicus status demo123 --json
|
|
|
348
348
|
"taskId": "demo123",
|
|
349
349
|
"status": "complete",
|
|
350
350
|
"elapsed": "5m 0s",
|
|
351
|
-
"version": "4.2.
|
|
351
|
+
"version": "4.2.1",
|
|
352
352
|
"model": "google/gemini-2.5-flash",
|
|
353
353
|
"phase": "terminal"
|
|
354
354
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
6
6
|
"keywords": [
|
|
@@ -60,6 +60,8 @@ function migrateEnvFileKey(envPath, oldName, newName) {
|
|
|
60
60
|
const updated = content.replace(re, `${newName}=`);
|
|
61
61
|
if (updated !== content) {
|
|
62
62
|
fs.writeFileSync(envPath, updated, { mode: 0o600 });
|
|
63
|
+
// Re-assert 0600 on the existing secrets file (writeFileSync mode is create-only).
|
|
64
|
+
try { fs.chmodSync(envPath, 0o600); } catch (_err) { /* perms best-effort */ }
|
|
63
65
|
}
|
|
64
66
|
} catch (_err) {
|
|
65
67
|
// Best effort
|
|
@@ -149,10 +151,11 @@ function saveApiKey(provider, key) {
|
|
|
149
151
|
if (!envVar) {
|
|
150
152
|
return { success: false, error: `Unknown provider: ${provider}` };
|
|
151
153
|
}
|
|
152
|
-
// Shared merge helper (preserves comments/other lines, dedups, 0600, trailing NL
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
process.env
|
|
154
|
+
// Shared merge helper (preserves comments/other lines, dedups, 0600, trailing NL;
|
|
155
|
+
// strips CR/LF and returns the cleaned value).
|
|
156
|
+
const clean = upsertEnvLine(getEnvPath(), envVar, key);
|
|
157
|
+
// Also set process.env so the key is immediately available (sanitized to match disk)
|
|
158
|
+
process.env[envVar] = clean;
|
|
156
159
|
return { success: true };
|
|
157
160
|
}
|
|
158
161
|
|
package/src/utils/env-loader.js
CHANGED
|
@@ -53,7 +53,6 @@ function loadCredentials() {
|
|
|
53
53
|
// process.env (same never-overwrite rule) so the engine's {env:VAR} finds it.
|
|
54
54
|
try {
|
|
55
55
|
const { getLocalProviders } = require('./local-providers');
|
|
56
|
-
const fileEntries = loadEnvEntries();
|
|
57
56
|
for (const entry of Object.values(getLocalProviders())) {
|
|
58
57
|
if (entry.apiKeyEnv && !process.env[entry.apiKeyEnv]) {
|
|
59
58
|
const v = fileEntries.get(entry.apiKeyEnv);
|
|
@@ -23,6 +23,10 @@ const ENV_VAR_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
|
23
23
|
* @param {string} value
|
|
24
24
|
*/
|
|
25
25
|
function upsertEnvLine(envPath, envVar, value) {
|
|
26
|
+
// Strip CR/LF: a newline in the value corrupts the line-based .env (splits it,
|
|
27
|
+
// or bakes a trailing CR into the persisted/served token). Bearer/API-key schemes
|
|
28
|
+
// never contain newlines, so stripping cannot damage a legitimate token.
|
|
29
|
+
const clean = String(value).replace(/[\r\n]/g, '');
|
|
26
30
|
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
27
31
|
|
|
28
32
|
let lines = [];
|
|
@@ -37,7 +41,7 @@ function upsertEnvLine(envPath, envVar, value) {
|
|
|
37
41
|
let found = false;
|
|
38
42
|
for (let i = 0; i < lines.length; i++) {
|
|
39
43
|
if (lines[i].trim().startsWith(envVar + '=')) {
|
|
40
|
-
lines[i] = `${envVar}=${
|
|
44
|
+
lines[i] = `${envVar}=${clean}`;
|
|
41
45
|
found = true;
|
|
42
46
|
break;
|
|
43
47
|
}
|
|
@@ -46,10 +50,14 @@ function upsertEnvLine(envPath, envVar, value) {
|
|
|
46
50
|
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
47
51
|
lines.pop();
|
|
48
52
|
}
|
|
49
|
-
lines.push(`${envVar}=${
|
|
53
|
+
lines.push(`${envVar}=${clean}`);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
fs.writeFileSync(envPath, lines.join('\n') + '\n', { mode: 0o600 });
|
|
57
|
+
// writeFileSync's mode only applies on CREATE; re-assert 0600 so an existing
|
|
58
|
+
// secrets file whose perms drifted is re-tightened. Best-effort (no-op on Windows).
|
|
59
|
+
try { fs.chmodSync(envPath, 0o600); } catch (_err) { /* perms best-effort */ }
|
|
60
|
+
return clean;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
@@ -68,6 +76,7 @@ function deleteEnvLine(envPath, envVar) {
|
|
|
68
76
|
lines.pop();
|
|
69
77
|
}
|
|
70
78
|
fs.writeFileSync(envPath, lines.length > 0 ? lines.join('\n') + '\n' : '', { mode: 0o600 });
|
|
79
|
+
try { fs.chmodSync(envPath, 0o600); } catch (_err) { /* perms best-effort */ }
|
|
71
80
|
}
|
|
72
81
|
} catch (_err) {
|
|
73
82
|
// Best-effort
|
|
@@ -89,8 +98,8 @@ function saveRawEnv(envVar, value) {
|
|
|
89
98
|
return { success: false, error: `Invalid env var name: ${envVar}` };
|
|
90
99
|
}
|
|
91
100
|
const { getEnvPath } = require('./api-key-store'); // lazy: avoids a load-time cycle
|
|
92
|
-
upsertEnvLine(getEnvPath(), envVar, value);
|
|
93
|
-
process.env[envVar] = value
|
|
101
|
+
const clean = upsertEnvLine(getEnvPath(), envVar, value);
|
|
102
|
+
process.env[envVar] = clean; // mirror the sanitized on-disk value
|
|
94
103
|
return { success: true };
|
|
95
104
|
}
|
|
96
105
|
|