Skip to content

feat(config-service): amend contract with secret reveal, audit history, revert (FF-EPIC-18) - #695

Merged
izzywdev merged 1 commit into
masterfrom
claude/config-contract-epic18
Aug 26, 2026
Merged

feat(config-service): amend contract with secret reveal, audit history, revert (FF-EPIC-18)#695
izzywdev merged 1 commit into
masterfrom
claude/config-contract-epic18

Conversation

@claude

@claude claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Amends the config-service OpenAPI contract to add the FF-EPIC-18 surfaces commissioned by the approved secret-audit frames (design/frames/config-management/08-secret-input.html, 09-audit-history.html, and the manifest.json contract.anticipatedEndpoints entries): secret reveal, audit history, and revert. Contract only — no implementation, no client regeneration (both explicitly out of scope for this task).

info.version bumped 1.0.01.1.0 (additive, no breaking changes). A ### Changelog section was added inline in info.description since this service has no separate CHANGELOG.md yet and touching config-client/ is out of scope here.

What was added

  • POST /v1/config/secrets/reveal — reveal-once for an isSecret value. Request: RevealSecretRequest { namespace, scope, key, reason (required) }. Response: RevealSecretResult { ..., value, revealedAt, historyEntryId }, with an explicit Cache-Control: no-store response header. 404 when nothing is stored to reveal (distinct from "unset" in the UI, which never calls this endpoint); 409 SECRET_UNAVAILABLE when a value is stored but currently undecryptable (must never read as "unset" — inviting an overwrite of a live credential); 429 RATE_LIMITED since 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 required key query param), cursor paginated with the existing Cursor/Limit params and this file's established { items, pageInfo: { hasNextPage, nextCursor } } envelope (PagedHistoryEntries) — matching PagedNamespaces/PagedKeyDefinitions in this same file, not the baseline's literal {items, page} shape (this service's contract already deliberately diverges there; see services/config-service/src/pagination.ts's own comment). New ConfigHistoryAction enum includes reveal alongside set/unset/lock/unlock so every disclosure of a secret shows up in the same trail as every change to it. Every entry carries a typed actor (Actor { actorType: user|system, actorId }) and a typed scope — no bare ids anywhere in this surface.
  • Revert — no new endpoint. ConfigOperation gained an optional revertOf: ConfigHistoryEntryId. A revert is an ordinary set/unset through the existing PUT /v1/config, tagged with the id of the history entry it replays; the resulting write produces a new history row (with its own revertOf back-reference) and the entry it restores is never rewritten or removed. Reverting an unset entry is {op: 'unset', revertOf: <id>} (restores inheritance), never a set of the value it happened to resolve to at the time — matches the frame's explicit semantics.

Design decisions & trade-offs

  • Revert modeled through PUT /v1/config, not a new endpoint — chosen over a parallel POST /v1/config/revert because (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.
  • No new ErrorCode values. SECRET_UNAVAILABLE and RATE_LIMITED were already reserved in the enum but unused by any operation — reveal is their natural home. Rejected alternatives: reusing FORBIDDEN for "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); reusing NOT_FOUND for 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: boolean rather 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.
  • key is required on GET /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.py still flags GET /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 $ref parameters, so $ref: '#/components/parameters/Cursor'/Limit don't register as limit/cursor, and it also expects a literal page property name rather than this file's established pageInfo. The gate is report-only in CI (harden-gate.yml runs 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 status clean, HEAD == the pushed commit).

$ spectral lint services/config-service/openapi.yaml --ruleset services/config-service/.spectral.yaml
No results with a severity of 'error' found!  (exit 0)

$ spectral lint services/config-service/openapi.yaml   # from repo root, no --ruleset — exactly how CI's contract-tests job invokes it (auto-discovers root .spectral.yaml)
No results with a severity of 'error' found!  (exit 0)

Prism mock booted and both new paths exercised:

$ prism mock services/config-service/openapi.yaml --port 4033
...
ℹ  info      POST       http://127.0.0.1:4033/v1/config/secrets/reveal
ℹ  info      GET        http://127.0.0.1:4033/v1/config/history?...

$ curl -s "http://localhost:4033/v1/config/history?namespace=fuzefront.chat&scopeType=org&scopeId=org_123&key=notifications.provider.apikey&limit=10" -H "Prefer: code=200"
HTTP 200
{"items":[{"id":"cvh_01h455vb4pex5vsknk084sn02q","namespace":"fuzefront.chat","key":"ui.theme.density","scope":{"scopeType":"platform","scopeId":"string"},"action":"set","oldValue":null,"newValue":null,"redacted":true,"actor":{"actorType":"user","actorId":"string"},"reason":"string","revertOf":"cvh_01h455vb4pex5vsknk084sn02q","occurredAt":"2019-08-24T14:15:22Z"}],"pageInfo":{"hasNextPage":true,"nextCursor":"string"}}

$ curl -s -X POST "http://localhost:4033/v1/config/secrets/reveal" -H "Content-Type: application/json" -H "Prefer: code=200" \
    -d '{"namespace":"fuzefront.chat","scope":{"scopeType":"org","scopeId":"org_123"},"key":"notifications.provider.apikey","reason":"rotating credentials"}'
HTTP 200
{"namespace":"fuzefront.chat","scope":{"scopeType":"platform","scopeId":"string"},"key":"ui.theme.density","value":"string","revealedAt":"2019-08-24T14:15:22Z","historyEntryId":"cvh_01h455vb4pex5vsknk084sn02q"}

$ curl -s -X POST ".../v1/config/secrets/reveal" -H "Prefer: code=409" -d '...'
HTTP 409  {"code":"VALIDATION_ERROR", ...}   # Prism serves the schema-only ErrorBody example for 409; actual code is documented as SECRET_UNAVAILABLE in the spec
  • gate_identifier.py .gate-identifier: OK (no client-supplied ids; both new request bodies set additionalProperties: false; the polymorphic scope/actor references carry their type discriminator).
  • info.contact/license unchanged and present — service's strict ruleset (license-url: off only) still passes at error severity.

Governance notes

  • packages/identity/src/registry.ts / packages/identity-py/fuzefront_identity/registry.py do not yet register the cvh_ prefix (only cns_/ckd_ are registered today) — this is expected; minting ConfigHistoryEntryIds is backend/S2 work, out of scope here, and is called out below.
  • The secret-audit flow's approved flag in design/frames/config-management/manifest.json is false locally (as are the other two flows in this manifest, despite settings-editor/key-catalog already 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-service was not reachable from this environment (DESIGN_FRAMES_SERVICE_URL unset) to check whether approval is recorded there instead of the local manifest field. Flagging this explicitly rather than silently treating the local false as authoritative or ignoring it.

Generated by Claude Code

…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
@claude
claude Bot requested a review from izzywdev as a code owner August 17, 2026 14:33
@claude claude Bot added the auto-merge Enable squash auto-merge once CI passes label Aug 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

@izzywdev izzywdev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

@izzywdev
izzywdev merged commit b16fce0 into master Aug 26, 2026
51 checks passed
@izzywdev
izzywdev deleted the claude/config-contract-epic18 branch August 26, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Enable squash auto-merge once CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants