feat(dashboard): list & add arbitrary custom .env keys on the Keys page - #54552
Conversation
The Keys page only rendered env vars present in a catalog (OPTIONAL_ENV_VARS or the provider catalog); any other key a user set in .env was invisible, and there was no way to add an arbitrary env var from the GUI (e.g. to inject a var a skill or MCP server needs). Backend: GET /api/env now also emits a row for every on-disk .env key that isn't in any catalog, flagged category="custom" + custom=true and password-masked (an unrecognised key could hold anything, so it's redacted and reveal-gated like any secret). Channel-managed credentials stay excluded. The write (PUT /api/env) and reveal (POST /api/env/reveal) paths already handle arbitrary keys, with the existing env-name guard + denylist (PATH, LD_PRELOAD, PYTHONPATH, …) enforced server-side — no new write surface. Frontend: a new "Custom Keys" section lists those custom rows and carries an add-a-key form (client-side name validation mirroring the backend regex; the new row reuses the normal edit/save flow, so on save it round-trips back from the backend as a durable custom row). i18n added for en + zh + types. Tests: behavior-contract coverage that an unknown .env key surfaces as a masked custom row and a catalogued key does not — verified to fail on the pre-fix backend.
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
1 |
First entries
tests/hermes_cli/test_env_custom_keys.py:11: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.testclient`
✅ Fixed issues: none
Unchanged: 6141 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Duplicate of #20808 (earliest open PR for this feature). Both surface unrecognised on-disk |
The env translation block is type-checked across every locale (tsc -b), so the 8 new customKeys strings must exist in all of them, not just en/zh. Add translated entries to the remaining 14 locales (de, es, fr, it, ja, ko, pt, ru, tr, uk, hu, ga, af, zh-hant).
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM
Well-structured feature that adds custom .env key management to the dashboard Keys page.
Key observations:
- Backend: _row() in get_env_vars() now accepts a custom flag and surfaces arbitrary .env keys as custom rows with is_password=True for security.
- Frontend: New UI section for custom keys with add/search functionality.
- i18n: 16 language files updated with consistent translations for the new feature.
- Tests: Dedicated test file (test_env_custom_keys.py) covers custom key surfacing, password masking, catalogued key exclusion, and custom flag presence.
- Security: Custom keys are treated as secrets by default (is_password=True, redacted, reveal-gated). Channel-managed credentials are excluded.
- File count: 21 files, but 16 are i18n translations (mechanical additions). Core changes are well-scoped to 5 files.
No security concerns. Clean implementation with good test coverage.
Reviewed by Hermes Agent
Infographic
What
The dashboard Keys page only rendered env vars present in a catalog
(
OPTIONAL_ENV_VARSor the provider catalog). Two gaps fall out of that:.envthat Hermes doesn't recognise wascompletely invisible on the page.
inject a var a skill, MCP server, or your own tooling needs.
Change
A new Custom Keys section on the Keys page that both lists unrecognised
on-disk keys and carries an add-a-key form.
Backend (
GET /api/env): after emitting catalogued rows, also emit a rowfor every on-disk
.envkey not in any catalog, flaggedcategory="custom"+custom=true. Custom rows are password-masked (is_password=true) — anunrecognised key could hold anything, so it's redacted in the listing and its
value is only retrievable through the existing token-gated, rate-limited,
audit-logged
POST /api/env/reveal. Channel-managed credentials stay excluded(they belong to the Channels page).
No new write surface, no new secret-exposure surface: the save
(
PUT /api/env) and reveal paths already accept arbitrary keys, and theexisting server-side env-name guard + denylist (
PATH,LD_PRELOAD,PYTHONPATH,LD_AUDIT, …) still reject anything that could escalate. Thelisting never ships plaintext — same redact/reveal model as every other key.
Frontend (
EnvPage.tsx): aCustomKeysCardlists the custom rows and anadd-key form takes a variable name (client-side validation mirroring the
backend
^[A-Za-z_][A-Za-z0-9_]*$regex), opens a normal editable row, andsaves through the existing
PUT /api/envflow — so on save the key round-tripsback from the backend as a durable custom row. i18n added for
en,zh, andtypes.ts.Why a section, not a raw
.envtextareaA bulk raw-
.enveditor would hand the browser every secret in plaintext inone GET, bypassing the redact-by-default + per-key reveal gate + reveal rate
limit + audit log the page deliberately enforces. The add-key form gets the
"inject arbitrary env vars" capability with zero new exposure — values stay
redacted and reveal stays gated.
Tests
tests/hermes_cli/test_env_custom_keys.py— behavior contracts against the realFastAPI app: an unknown
.envkey surfaces as acustom, set, password-maskedrow whose value never appears in the listing payload; a catalogued key
(
HONCHO_API_KEY) is not mislabelled custom; every row carries thecustomflag. Verified to fail on the pre-fix backend (3/4 fail) and pass with this
change. The frontend typechecks (
tsc -p web --noEmit), and an E2E round-tripagainst a temp
HERMES_HOMEconfirms add → list-as-custom → reveal works.Infographic (merge)