feat(config-service): amend contract with secret reveal, audit history, revert (FF-EPIC-18) - #695
Merged
Merged
Conversation
…y, revert (FF-EPIC-18) Adds POST /v1/config/secrets/reveal (reveal-once, separately authorized and individually audited) and GET /v1/config/history (append-only, redacted for secrets, cursor paginated) commissioned by the approved secret-audit frames (design/frames/config-management/08-secret-input.html, 09-audit-history.html). Revert is expressed through the existing PUT /v1/config vocabulary via a new optional ConfigOperation.revertOf reference rather than a parallel mutation path, so a revert is itself recorded as a new history entry. No new ErrorCode values — SECRET_UNAVAILABLE (409) and RATE_LIMITED (429), both already reserved and previously unused, now have operations that answer with them. info.version 1.0.0 -> 1.1.0, additive only. Contract only — no implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0183JMAkioT5pGt8aVmddPtc
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
izzywdev
approved these changes
Aug 26, 2026
izzywdev
left a comment
Owner
There was a problem hiding this comment.
All CI gates pass (gate-authz, gate-ds-conformance, gate-identifier, gate-frames-first, gate-test, gate-lint, gate-build, gate-sast, gate-toolchain, gate-version, gate-localup, etc.). Approving per governance policy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Amends the
config-serviceOpenAPI contract to add the FF-EPIC-18 surfaces commissioned by the approvedsecret-auditframes (design/frames/config-management/08-secret-input.html,09-audit-history.html, and themanifest.jsoncontract.anticipatedEndpointsentries): secret reveal, audit history, and revert. Contract only — no implementation, no client regeneration (both explicitly out of scope for this task).info.versionbumped1.0.0→1.1.0(additive, no breaking changes). A### Changelogsection was added inline ininfo.descriptionsince this service has no separateCHANGELOG.mdyet and touchingconfig-client/is out of scope here.What was added
POST /v1/config/secrets/reveal— reveal-once for anisSecretvalue. Request:RevealSecretRequest { namespace, scope, key, reason (required) }. Response:RevealSecretResult { ..., value, revealedAt, historyEntryId }, with an explicitCache-Control: no-storeresponse header.404when nothing is stored to reveal (distinct from "unset" in the UI, which never calls this endpoint);409 SECRET_UNAVAILABLEwhen a value is stored but currently undecryptable (must never read as "unset" — inviting an overwrite of a live credential);429 RATE_LIMITEDsince a successful call discloses a live credential.GET /v1/config/history— the append-only trail for one key at one exact scope (namespace+scopeType+scopeId+ new requiredkeyquery param), cursor paginated with the existingCursor/Limitparams and this file's established{ items, pageInfo: { hasNextPage, nextCursor } }envelope (PagedHistoryEntries) — matchingPagedNamespaces/PagedKeyDefinitionsin this same file, not the baseline's literal{items, page}shape (this service's contract already deliberately diverges there; seeservices/config-service/src/pagination.ts's own comment). NewConfigHistoryActionenum includesrevealalongsideset/unset/lock/unlockso every disclosure of a secret shows up in the same trail as every change to it. Every entry carries a typedactor(Actor { actorType: user|system, actorId }) and a typedscope— no bare ids anywhere in this surface.ConfigOperationgained an optionalrevertOf: ConfigHistoryEntryId. A revert is an ordinaryset/unsetthrough the existingPUT /v1/config, tagged with the id of the history entry it replays; the resulting write produces a new history row (with its ownrevertOfback-reference) and the entry it restores is never rewritten or removed. Reverting anunsetentry is{op: 'unset', revertOf: <id>}(restores inheritance), never asetof the value it happened to resolve to at the time — matches the frame's explicit semantics.Design decisions & trade-offs
PUT /v1/config, not a new endpoint — chosen over a parallelPOST /v1/config/revertbecause (a) the frames explicitly commission it this way ("Revert is NOT a new endpoint"), (b) it reuses all existing write validation (locks,allowedScopes,expectedVersion, atomicity) for free instead of re-deriving it for a second mutation path, and (c) it keeps "what changed" as a single, uniform audit source (writeConfigValues→ history) rather than two write paths that could drift.ErrorCodevalues.SECRET_UNAVAILABLEandRATE_LIMITEDwere already reserved in the enum but unused by any operation — reveal is their natural home. Rejected alternatives: reusingFORBIDDENfor "temporarily undecryptable" would collapse it into an authorization denial (the frames explicitly require these to read differently — "must NOT be presented as not set" and must not be confused with a permission refusal); reusingNOT_FOUNDfor the same case would read as "nothing was ever set," inviting an operator to overwrite a working credential — the exact mistake the frame's SECRET_UNAVAILABLE state exists to prevent.redacted: booleanrather than trying to distinguish "no prior value" from "prior value existed but is redacted." The frame mockup shows a cosmetic difference (— → [redacted]vs[redacted] → [redacted]) but the acceptance notes only require redaction on both sides, not that nuance — added complexity wasn't justified by anything load-bearing in the spec.keyis required onGET /v1/config/history, matching the frame's route (/admin/config/keys/:key/history) and the manifest's own description ("per (namespace, scope, key)"). A scope-wide, all-keys history view is not modeled here; if needed later it re-enters through a contract amendment.gate_pagination.pystill flagsGET /v1/config/history, exactly as it already (pre-existing, unrelated to this change) flags the two existing list endpoints in this file — the script does not dereference$refparameters, so$ref: '#/components/parameters/Cursor'/Limitdon't register aslimit/cursor, and it also expects a literalpageproperty name rather than this file's establishedpageInfo. The gate is report-only in CI (harden-gate.ymlruns it with|| true) and this is not a regression — the new endpoint follows the exact same, already-shipped convention as the endpoints it sits beside.Verification (commands + real output)
Committed tree verified clean before linting (
git statusclean,HEAD== the pushed commit).Prism mock booted and both new paths exercised:
gate_identifier.py .→gate-identifier: OK(no client-supplied ids; both new request bodies setadditionalProperties: false; the polymorphicscope/actorreferences carry their type discriminator).info.contact/licenseunchanged and present — service's strict ruleset (license-url: offonly) still passes at error severity.Governance notes
packages/identity/src/registry.ts/packages/identity-py/fuzefront_identity/registry.pydo not yet register thecvh_prefix (onlycns_/ckd_are registered today) — this is expected; mintingConfigHistoryEntryIds is backend/S2 work, out of scope here, and is called out below.secret-auditflow'sapprovedflag indesign/frames/config-management/manifest.jsonisfalselocally (as are the other two flows in this manifest, despitesettings-editor/key-catalogalready backing the merged 1.0.0 contract, PR feat(config): freeze the config-service contract and ship @fuzefront/config-client (FFRNT-153) #608).design-frames-servicewas not reachable from this environment (DESIGN_FRAMES_SERVICE_URLunset) to check whether approval is recorded there instead of the local manifest field. Flagging this explicitly rather than silently treating the localfalseas authoritative or ignoring it.Generated by Claude Code