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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.2.0",
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
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?labelColor=1A1C29)](https://nodejs.org)
16
16
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?labelColor=1A1C29)](./CONTRIBUTING.md)
17
17
 
18
- > **Supported clients:** Claude Code CLI and Claude Cowork are fully tested and supported. Claude Code web and Claude Desktop are experimental.
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.0",
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.0",
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
- upsertEnvLine(getEnvPath(), envVar, key);
154
- // Also set process.env so the key is immediately available
155
- process.env[envVar] = key;
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
 
@@ -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}=${value}`;
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}=${value}`);
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