Skip to content

feat: load secrets from systemd credentials ($CREDENTIALS_DIRECTORY)#743

Open
MarvinFS wants to merge 2 commits into
rohitg00:mainfrom
MarvinFS:feat/systemd-credentials-upstream
Open

feat: load secrets from systemd credentials ($CREDENTIALS_DIRECTORY)#743
MarvinFS wants to merge 2 commits into
rohitg00:mainfrom
MarvinFS:feat/systemd-credentials-upstream

Conversation

@MarvinFS
Copy link
Copy Markdown

@MarvinFS MarvinFS commented May 31, 2026

Problem

Run agentmemory as a systemd service and the only ways to pass provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, AGENTMEMORY_SECRET, ...) are Environment= / EnvironmentFile= - both place the secrets in the process environment, readable via /proc/<pid>/environ and plaintext at rest.

Change

An opt-in loader, loadSystemdCredentials(), called once at the start of the worker main(). It copies any files in $CREDENTIALS_DIRECTORY into process.env (only when unset/empty; trailing newline trimmed; file name maps 1:1 to the env var). A value assigned to process.env after exec is readable in-process but is not written back to the exec environ block, so it does not appear in /proc/<pid>/environ (verified on Node 22 / glibc). The providers get the keys; the environment stays clean.

Requirements & platform support

Purely additive and a no-op when $CREDENTIALS_DIRECTORY is unset, so non-systemd hosts (macOS, Windows, Alpine/OpenRC) and existing Environment=/.env setups are entirely unaffected.

  • Encrypted at rest: LoadCredentialEncrypted= + systemd-creds - systemd >= 250 (Debian 12+, RHEL/Rocky/Alma 9+, Fedora, Arch, openSUSE, Ubuntu 24.04+).
  • Out-of-environment but plaintext at rest: LoadCredential= - systemd >= 247 (e.g. Ubuntu 22.04).
  • systemd < 247 or no systemd: loader no-ops; use the existing env approach.

Enabling it (operator, systemd >= 250)

systemd-creds encrypt --name=ANTHROPIC_API_KEY anthropic.key \
  /etc/credstore.encrypted/ANTHROPIC_API_KEY.cred
# unit / drop-in
[Service]
LoadCredentialEncrypted=ANTHROPIC_API_KEY:/etc/credstore.encrypted/ANTHROPIC_API_KEY.cred

systemd decrypts into a private tmpfs ($CREDENTIALS_DIRECTORY, mode 0400) at start; the loader reads it from there.

Compatibility

Explicitly set env vars always win. Unit tests cover populate/trim, no-overwrite, fill-empty, unset/missing dir, and ignoring subdirs/empty files.

Summary by CodeRabbit

  • New Features

    • App now automatically loads credentials delivered by systemd at startup, populating environment variables for downstream configuration while preserving existing non-empty values.
  • Tests

    • Added comprehensive tests covering credential loading behaviors (trimming, no-ops for missing dirs, ignoring subdirectories, and preserving/restoring environment state).

Add loadSystemdCredentials(), called once at the start of the worker
main(). It copies any files systemd places in $CREDENTIALS_DIRECTORY
into process.env (only when unset/empty; trailing newline trimmed;
file name maps 1:1 to the env var), letting operators pass provider
keys via systemd's native credential mechanism (LoadCredential=,
LoadCredentialEncrypted=) instead of Environment=/EnvironmentFile=.

Unlike the environment, credentials are decrypted into a private tmpfs
and never placed in the process environment, so they do not appear in
/proc/<pid>/environ. A value assigned to process.env after exec is
readable in-process but is not written back to the exec environ block
(verified on Node 22/glibc), so loading at runtime does not reintroduce
the exposure.

Fully opt-in and a no-op when $CREDENTIALS_DIRECTORY is unset, so
non-systemd hosts and existing Environment=/.env setups are unaffected;
explicitly set vars always win. Adds unit tests.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

@MarvinFS is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 31, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff2f4221-8e66-4ee0-9423-b25a27092f22

📥 Commits

Reviewing files that changed from the base of the PR and between 2c4730d and 558b003.

📒 Files selected for processing (1)
  • test/load-credentials.test.ts
💤 Files with no reviewable changes (1)
  • test/load-credentials.test.ts

📝 Walkthrough

Walkthrough

Adds a new loadSystemdCredentials() to read files from CREDENTIALS_DIRECTORY into process.env, calls it at the start of main() before config/provider setup, and introduces tests covering reading, trimming, overwrite behavior, empty files, and missing-directory no-ops.

Changes

Systemd Credentials Integration

Layer / File(s) Summary
Load credentials implementation
src/load-credentials.ts
New loadSystemdCredentials() reads process.env.CREDENTIALS_DIRECTORY, iterates directory entries, and populates unset or empty environment variables with trimmed file contents while silently ignoring read failures and non-file entries.
Integration into application startup
src/index.ts
Import loadSystemdCredentials and call it at the start of main() before loadConfig() and provider initialization so systemd-delivered secrets are available to downstream startup logic.
Test coverage for credentials loading
test/load-credentials.test.ts
Vitest suite with temporary directory fixtures verifies credential file reading and trimming, preserves non-empty env vars, fills empty env vars, and ensures no-op behavior when CREDENTIALS_DIRECTORY is unset or missing; ignores subdirectories and empty files.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I found small secrets in a quiet fold,
I trimmed their edges, kept the stories bold,
I set the ones that waited, left the rest alone,
Soft paws in startup, bringing secrets home.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding support for loading secrets from systemd credentials via the $CREDENTIALS_DIRECTORY mechanism.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/index.ts (1)

161-163: 💤 Low value

Drop the startup behavior comment here.

This is another WHAT-style source comment. The call site is self-explanatory once loadSystemdCredentials() is named clearly; keep the operational detail in docs if it needs to be preserved.

As per coding guidelines, src/**/*.{ts,js}: No code comments explaining WHAT — use clear naming instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 161 - 163, Remove the WHAT-style startup behavior
comment block and rely on the clear function name loadSystemdCredentials() at
the call site; delete the explanatory lines describing systemd-delivered secrets
and the CREDENTIALS_DIRECTORY mechanics and leave the call (or a short one-line
comment only if needed for intent like "load systemd credentials") so
operational details remain in external docs rather than inline.
src/load-credentials.ts (1)

4-24: 💤 Low value

Move the operator-facing behavior notes out of src/.

These comments explain what the loader does and how it works operationally. The function name is already clear; keep the code terse and move the systemd /proc rationale to README or operator docs instead.

As per coding guidelines, src/**/*.{ts,js}: No code comments explaining WHAT — use clear naming instead.

Also applies to: 45-46

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/load-credentials.ts` around lines 4 - 24, Remove the long operator-facing
systemd/proc rationale block from src/load-credentials.ts and move that content
into README or operator docs; in the source file keep a concise JSDoc that
states the loader's purpose (bridge credential files into process.env) and the
essential behavior points only: it maps credential filenames to process.env
vars, only fills blank env vars, and is a no-op when $CREDENTIALS_DIRECTORY is
unset. Update the file-level comment near the top of src/load-credentials.ts
(and the similar note at lines 45-46) to the terse version and place the
detailed operational explanation in documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/load-credentials.test.ts`:
- Around line 21-22: The cleanup currently unconditionally deletes
process.env["CREDENTIALS_DIRECTORY"], which can remove a pre-existing value;
change the cleanup to only delete CREDENTIALS_DIRECTORY if it wasn't present in
the savedEnv snapshot (i.e., if savedEnv does not have the CREDENTIALS_DIRECTORY
key), otherwise restore the original value from savedEnv so the environment is
returned to its prior state; refer to the savedEnv variable and
process.env["CREDENTIALS_DIRECTORY"] in your change.

---

Nitpick comments:
In `@src/index.ts`:
- Around line 161-163: Remove the WHAT-style startup behavior comment block and
rely on the clear function name loadSystemdCredentials() at the call site;
delete the explanatory lines describing systemd-delivered secrets and the
CREDENTIALS_DIRECTORY mechanics and leave the call (or a short one-line comment
only if needed for intent like "load systemd credentials") so operational
details remain in external docs rather than inline.

In `@src/load-credentials.ts`:
- Around line 4-24: Remove the long operator-facing systemd/proc rationale block
from src/load-credentials.ts and move that content into README or operator docs;
in the source file keep a concise JSDoc that states the loader's purpose (bridge
credential files into process.env) and the essential behavior points only: it
maps credential filenames to process.env vars, only fills blank env vars, and is
a no-op when $CREDENTIALS_DIRECTORY is unset. Update the file-level comment near
the top of src/load-credentials.ts (and the similar note at lines 45-46) to the
terse version and place the detailed operational explanation in documentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e8a5c46-a70b-41c7-bba4-e9c91f2089cf

📥 Commits

Reviewing files that changed from the base of the PR and between fd9e3bd and 2c4730d.

📒 Files selected for processing (3)
  • src/index.ts
  • src/load-credentials.ts
  • test/load-credentials.test.ts

Comment thread test/load-credentials.test.ts Outdated
The afterEach loop plus Object.assign(savedEnv) already restores the environment exactly; the extra unconditional delete dropped a CREDENTIALS_DIRECTORY that was set before the suite ran, leaking state into later tests. (CodeRabbit feedback on rohitg00#743)
@MarvinFS
Copy link
Copy Markdown
Author

Addressed the cleanup feedback in 558b003: the afterEach loop plus Object.assign(savedEnv) already restores the environment exactly, so the redundant unconditional delete process.env["CREDENTIALS_DIRECTORY"] (which could clobber a value the runner had pre-set) is removed.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 31, 2026

@MarvinFS: Thanks for the cleanup fix in 558b003 — using Object.assign(savedEnv) to fully restore the environment is cleaner and correctly handles the case where CREDENTIALS_DIRECTORY was pre-set in the runner. I'll kick off a review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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