Skip to content

fix(security): private settings permissions + hash apiKey cache key - #267

Open
raymondginger2018-sudo wants to merge 3 commits into
lessweb:mainfrom
raymondginger2018-sudo:fix/private-settings-and-key-hash
Open

fix(security): private settings permissions + hash apiKey cache key#267
raymondginger2018-sudo wants to merge 3 commits into
lessweb:mainfrom
raymondginger2018-sudo:fix/private-settings-and-key-hash

Conversation

@raymondginger2018-sudo

Copy link
Copy Markdown

Two P0 hardening fixes:

1. settings.json written with user-only permissions

~/.deepcode/settings.json holds the API key but was written with the default permissive mode (POSIX umask) / profile ACL. On Windows, Authenticated Users can read it by default.

  • New core/common/private-storage.ts:
    • writePrivateFile — 0600 on POSIX; icacls /inheritance:r + /grant:r <user>:F (current-user exclusive) on Windows.
    • ensurePrivateDirectory — 0700 / current-user ACL.
  • writeSettingsFile now routes through these helpers.

2. openai-client no longer keeps the raw apiKey in module state

The client cache key was \`${settings.apiKey}::${settings.baseURL}\ — the raw secret lingered in a module-level variable, so a heap dump or crash report contained it verbatim.

  • Cache key is now sha256(apiKey)[:16]::baseURL.

Tests

packages/core/src/tests/private-storage.test.ts — POSIX 0600/0700 modes, Windows ACL restriction, idempotency.

Full core suite: 284 tests, 277 pass, 0 fail (7 pre-existing skips). tsc --noEmit clean.

Two P0 hardening fixes:

1. settings.json now written with user-only permissions
   - New core/common/private-storage.ts: writePrivateFile (0600 on POSIX,
     icacls /inheritance:r + /grant:r <user>:F on Windows) and
     ensurePrivateDirectory (0700 / current-user ACL).
   - writeSettingsFile routes through these helpers; previously the API key
     landed in ~/.deepcode/settings.json with the default permissive mode /
     profile ACL (Authenticated Users can read it on Windows).

2. openai-client no longer keeps the raw apiKey in module state
   - Cache key is now sha256(apiKey)[:16] + baseURL instead of the plaintext
     apiKey, so a heap dump or crash report never contains the secret.

Tests (private-storage.test.ts): POSIX 0600/0700 modes, Windows ACL
restriction, idempotency. Full suite: 284 tests, 0 failures.
P1/P2 hardening on top of the private-settings fix:

1. Atomic session writes (session.ts)
   - saveSessionsIndex / saveSessionMessages now write via
     writeFileAtomic (tmp + rename) instead of writeFileSync, so a crash
     mid-write never leaves a truncated sessions-index.json / .jsonl.

2. MCP error classification (mcp-manager.ts)
   - classifyMcpError() tags tool failures as timeout/connection/auth/
     protocol/busy so the agent can distinguish a wedged server (retry)
     from a config error (don't retry). Surface as mcpErrorKind on the
     tool result.

3. Background log sweep (bash-handler.ts)
   - sweepOldBackgroundLogs() removes deepcode-background/*.log older than
     7 days on each background start (was never cleaned).

Evaluated and left as-is: sessionWorkingDirs is already cleared on session
delete; deps already use package-lock.json + npm ci in CI.

Tests: hardening.test.ts (atomic write, classifyMcpError, sweep) — 9 new
cases. Full suite: 293 tests, 286 pass, 0 fail. tsc --noEmit clean.
@raymondginger2018-sudo

Copy link
Copy Markdown
Author

Extended this PR with P1/P2 hardening (commits added to the branch):

P1

  • Atomic session writes — / now write via tmp+rename (), so a crash mid-write never leaves a truncated / .
  • MCP error classification — tags tool failures as //// (surfaced as ), so the agent can distinguish a wedged server (retry) from a config error (don't retry).

P2

  • Background log sweep — removes older than 7 days on each background start (they were never cleaned).

Evaluated, no change needed

  • is already cleared on session delete.
  • Dependencies already use + in CI.

Full core suite: 293 tests, 286 pass, 0 fail (7 pre-existing skips). clean.

…le-response total

activeTokens was set to getTotalTokens(responseUsage), i.e. the token count
of only the latest response. Since each individual call stays below the
autoCompactWindow threshold, session.activeTokens never crosses it and
auto-compaction never triggers. Long sessions keep resending their full
history on every turn, causing runaway token usage.

Fix:
- activeTokens now accumulates: (entry.activeTokens ?? 0) + getTotalTokens(responseUsage)
- reset activeTokens to 0 after compaction so it does not re-trigger immediately

Add regression tests (auto-compact.test.ts) and update the two existing
session tests that asserted the old single-response behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant