Config redaction now covers all secret fields
A security fix ensures configuration snapshots no longer expose plaintext secrets through two alias fields that were inadvertently skipping redaction.
The redactConfigSnapshot function contained a gap in its redaction logic. When processing configuration snapshots, the function spread the original object and then overwrote only the config and resolved fields with redacted versions. However, two newer canonical alias fields—sourceConfig and runtimeConfig—were copied by reference from the original snapshot, bypassing redaction entirely.
This mattered because the config.get gateway RPC serializes full snapshot output to authenticated clients. Anyone with operator.read scope could potentially extract stored secrets including API keys, authentication tokens, and channel credentials through these unredacted fields.
The fix explicitly overwrites both sourceConfig and runtimeConfig with their corresponding redacted values. In the valid-snapshot path, sourceConfig receives redactedResolved and runtimeConfig receives redactedConfig. The invalid-snapshot path now uses named constants for the empty redacted objects, then assigns them consistently across all four fields.
Referential equality is preserved—sourceConfig remains identical to resolved, and runtimeConfig remains identical to config—so downstream code relying on reference checks continues to work correctly.
Tests now verify both alias fields contain REDACTED_SENTINEL and maintain their expected identity relationships.
View Original GitHub Description
Summary
- Problem:
redactConfigSnapshot()spread...snapshotthen only overwroteconfigandresolvedwith redacted copies, leavingsourceConfigandruntimeConfig(the canonical alias fields) pointing to the original unredacted objects. Any caller reading those fields received plaintext secrets. - Why it matters: The
config.getgateway RPC serializes the full snapshot output to authenticated clients. A session withoperator.readscope could extract all stored secrets (API keys, auth tokens, channel credentials) via the unredacted alias fields. - What changed:
redactConfigSnapshotnow explicitly overwritessourceConfig(set toredactedResolved) andruntimeConfig(set toredactedConfig) in both the valid-snapshot and invalid-snapshot return paths. Test coverage added for both new fields. - What did NOT change: Redaction logic for
config,resolved,raw, andparsedis unchanged. No gateway RPC surface, scope model, or schema changed.
AI-assisted: This fix was generated by OpenAI Codex and reviewed prior to submission.
Change Type (select all)
- Bug fix
- Feature
- Refactor required for the fix
- Docs
- Security hardening
- Chore/infra
Scope (select all touched areas)
- Gateway / orchestration
- Skills / tool execution
- Auth / tokens
- Memory / storage
- Integrations
- API / contracts
- UI / DX
- CI/CD / infra
Linked Issue/PR
- This PR fixes a bug or regression
Root Cause (if applicable)
- Root cause:
redactObject()returns new objects rather than mutating in place. The object spread copies the originalsourceConfigandruntimeConfigreferences, and the subsequent explicit overrides only covered the deprecated aliases (config,resolved), not the canonical fields. - Missing detection / guardrail: No test asserted
result.sourceConfigorresult.runtimeConfigwere redacted — only the deprecated alias fields were checked. - Contributing context:
sourceConfig/runtimeConfigwere introduced as canonical replacements forresolved/configbut the redaction function was not updated in lockstep.
Regression Test Plan (if applicable)
- Coverage level that should have caught this:
- Unit test
- Target test or file:
src/config/redact-snapshot.test.ts - Scenario the test should lock in: Both
result.sourceConfigandresult.runtimeConfigcontainREDACTED_SENTINELfor known-sensitive values; both alias pairs are referentially equal (sourceConfig === resolved,runtimeConfig === config). - Why this is the smallest reliable guardrail: Directly exercises the output shape of
redactConfigSnapshotwithout any gateway layer. - Existing test that already covered this: None — only
result.configandresult.resolvedwere previously asserted.
User-visible / Behavior Changes
None. sourceConfig and runtimeConfig were already supposed to be redacted; this makes the behavior match the documented contract.
Diagram (if applicable)
Before:
redactConfigSnapshot() -> { ...snapshot, // sourceConfig/runtimeConfig: UNREDACTED
config: redacted,
resolved: redacted }
After:
redactConfigSnapshot() -> { ...snapshot,
sourceConfig: redacted, // now explicit
runtimeConfig: redacted, // now explicit
config: redacted,
resolved: redacted }
Security Impact (required)
- New permissions/capabilities? No
- Secrets/tokens handling changed? Yes —
sourceConfigandruntimeConfigfields are now properly redacted before being returned byredactConfigSnapshot. - New/changed network calls? No
- Command/tool execution surface changed? No
- Data access scope changed? No
- Risk + mitigation: The change narrows data exposure. The only risk is if a caller depended on receiving unredacted values via these fields, which would be an incorrect usage against the documented contract.
Repro + Verification
Environment
- OS: Linux (CI)
- Runtime/container: Node 22 / Bun
- Model/provider: N/A
- Integration/channel: N/A
- Relevant config: Any config with
gateway.auth.tokenorchannels.discord.tokenset
Steps
- Run
pnpm test src/config/redact-snapshot.test.ts - Observe new assertions pass:
result.sourceConfigandresult.runtimeConfigboth containREDACTED_SENTINEL - Observe identity assertions pass:
result.sourceConfig === result.resolved,result.runtimeConfig === result.config
Expected
- All redaction tests pass including the new alias-field assertions
Actual
- All tests pass
Evidence
- Failing test/log before + passing after
New test cases added to redact-snapshot.test.ts directly assert the previously-unredacted fields now contain REDACTED_SENTINEL.
Human Verification (required)
- Verified scenarios: Valid-snapshot redaction path (both alias fields redacted); invalid-snapshot branch (both alias fields return
{}); referential equality maintained between alias pairs. - Edge cases checked:
sourceConfig === resolvedandruntimeConfig === configidentity preserved so downstream code relying on reference equality is unaffected. - What you did not verify: Live gateway RPC round-trip with a real scoped session.
Review Conversations
- I replied to or resolved every bot review conversation I addressed in this PR.
- I left unresolved only the conversations that still need reviewer or maintainer judgment.
Compatibility / Migration
- Backward compatible? Yes
- Config/env changes? No
- Migration needed? No
Risks and Mitigations
- Risk: A caller that expected
sourceConfig/runtimeConfigto be unredacted (incorrect usage) would now receive redacted values.- Mitigation: No such caller exists in the codebase; these fields are exclusively read-only outputs of
redactConfigSnapshotand no code path depends on receiving unredacted aliases from this function.
- Mitigation: No such caller exists in the codebase; these fields are exclusively read-only outputs of