feat(prompts): versioning, approval provenance, and usage stats#1021
Merged
Conversation
Every database prompt is versioned: prompt_versions (migration 000085) snapshots content, display name, description, arguments, and tags on each mutation with a monotonic per-prompt version number and author, and approval stamps bind immutably to the specific version approved. The live prompts row carries the version it serves; system rows (config mirrors) never version. Editing the content or arguments of an approved global or persona prompt no longer changes what is served: the shared review gate (prompt.ApplyEdit, used by manage_prompt, admin REST, and portal REST) saves the edit as a pending draft version, and the approved snapshot keeps serving until an admin approves the draft. The postgres store re-validates the gate under the row lock, so an edit racing an approval conflicts (prompt.ErrVersionConflict) instead of slipping unreviewed content through. Metadata-only edits apply directly; personal prompts version silently. Mixed gated edits are rejected whole. New REST surface in pkg/prompt/versionhttp, mounted by the composition root under each surface's own auth middleware: admin version list/get/approve/ reject and usage rollup, portal owner-readable version history and visible-scope usage. The portal prompt mutation paths now also enforce the system-row read-only guard. Served prompts carry provenance: prompts/get stamps prompt_version, prompt_approved_by, prompt_approved_at, and prompt_reference into _meta, and manage_prompt use/get report version and audit-derived usage. Every database-prompt serve emits a prompt_serve audit event; the audit store aggregates run count and last-run per prompt behind a partial index. Existing prompts migrate as v1 with their approval stamps carried over. A real-Postgres integration test proves a draft is not served until approved and that approval provenance travels to the client.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1021 +/- ##
========================================
Coverage 89.30% 89.31%
========================================
Files 458 465 +7
Lines 50757 51375 +618
========================================
+ Hits 45330 45885 +555
- Misses 3615 3649 +34
- Partials 1812 1841 +29 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Closes #1009. Part 2 of the prompt-library overhaul, building on the stable invocation identity from #1008 (#1019); the portal library reorganization (#1010) and the prompt browser MCP App (#1011) surface what this records.
Problem
The prompt lifecycle recorded who approved a prompt and when, but not what they approved: an approved prompt's content could be edited in place with no re-review and no history, so "approved by jane@ on 2026-06-12" said nothing about the content being served a month later. There was also no usage signal to distinguish the prompts that run daily from the ones that are dead.
What this adds
Every database prompt is versioned. A new
prompt_versionstable (migration 000085) snapshots the reviewable substance of a prompt (content, display name, description, arguments, tags) on each mutation, with a monotonically increasing per-prompt version number, the author, and the approval stamp bound to that specific version. The livepromptsrow remains the served state and carries the number of the snapshot it serves (prompts.version). Existing prompts migrate as v1 with their current content and approval stamps carried over; system rows (read-only config mirrors re-ingested at startup) are never versioned.Editing an approved shared prompt re-enters review. A content or arguments edit to an approved global or persona prompt does not change what is served: it lands as a
draftversion row, and every caller keeps being served the approved snapshot until an admin approves the draft.manage_prompt updatereturnsstatus: "pending_approval"with the draft'spending_version; the admin and portal REST update endpoints return 202 with the same outcome. Approving a draft applies its snapshot to the live row, stamps the approval on both the prompt and that version row, and supersedes any other pending drafts; approving v5 never alters v4's recorded approval. Metadata-only edits (tags, category, description, display name) apply directly and still snapshot; personal prompts version silently since the owner is the only consumer. A gated content edit combined with non-versioned changes (scope, status, rename, and so on) is rejected whole with instructions to submit them separately, so a draft is always exactly the reviewable snapshot.One gate, all mutation surfaces. The review decision lives in a single shared pipeline,
prompt.ApplyEdit, used identically by themanage_prompttool, the admin REST handler, and the portal REST handler. The gate is also a store-level invariant:UpdateandUpdateWithVersionre-validate it against the row as locked in the transaction, so an edit racing a concurrent approval is rejected as a conflict (prompt.ErrVersionConflict) instead of slipping unreviewed content into an approved prompt.CreateDraftVersionrefuses system rows, and the portal prompt update/delete paths now enforce the same system-row read-only guard the admin API and manage_prompt already had.Provenance travels with the prompt.
prompts/getresponses stampprompt_version,prompt_approved_by,prompt_approved_at, andprompt_referenceinto_meta;manage_prompt useincludesversionin its provenance block. An agent can state "running Daily Sales Report v4, approved by jane@example.com on 2026-06-12" before executing.Usage stats from audit. Every database-prompt serve (a
prompts/geton a database prompt, or a resolvedmanage_prompt use) emits aprompt_serveaudit event carrying the prompt id, name, and version. The audit store aggregates these into per-prompt run counts and last-run timestamps (within the audit retention window), kept index-eligible by a partial expression index on(parameters->>'prompt_id') WHERE event_kind = 'prompt_serve'.manage_prompt getreportsrun_countandlast_run_at, and bulk rollup endpoints back the portal library view. No new tracking pipeline.New REST surface
GET /api/v1/admin/prompts/{id}/versionsGET /api/v1/admin/prompts/{id}/versions/{version}POST /api/v1/admin/prompts/{id}/versions/{version}/approvePOST /api/v1/admin/prompts/{id}/versions/{version}/rejectGET /api/v1/admin/prompts/usageGET /api/v1/portal/prompts/{id}/versionsGET /api/v1/portal/prompts/usageThe handlers live in the new
pkg/prompt/versionhttppackage and are mounted by the composition root under each surface's own authentication middleware (admin.RequirePersona,portal.RequirePortalAuth), so persona, CSRF, and cookie/token rules are identical to the surfaces' native routes. State conflicts surface as 409 with the store's message; infrastructure failures are a generic 500 so driver detail never reaches a client. Approval stamps fall back to the API-key name for admins authenticating without an email, so an approval is always attributable.Design notes
prompt.VersionStoreinterface implemented by the postgres store and preserved through the prompt layer's notifying wrapper (list_changed fires on the version writes that change served content: applied updates and approvals, not draft creation or rejection).ApplyEditasserts the capability from the store itself and degrades to a plain unversioned update for stores without it, so no wiring changes were needed onPlatformand no new facade method was added.UNIQUE (prompt_id, version)backstops it. Version numbers are a mutation sequence: approving a draft numbered below the live version (possible when a metadata edit lands after the draft) sets the live row to exactly the approved snapshot's number.pkg/adminandpkg/portalbecause both packages sit at the package-size budget; the portal's inline delete permission checks were consolidated into the sharedcheckPortalPromptPermissionin the process.Verification
make verifypasses (full CI-equivalent suite; patch coverage 90.8% against the 80% gate, every new function at or above 80%).make test-realdbpasses. New real-Postgres tests cover the full lifecycle (create v1, applied snapshots, draft below the live version, approval stamps immutable across later approvals, supersede, reject, FK cascade) and an assembled-system acceptance test: through a realmcp.Serverwith the prompt-visibility middleware, an admin's content edit via the realmanage_prompthandler leaves another user served the approved snapshot, and only approving the draft flips the served content and_metaprovenance.Docs
docs/server/tools.md(manage_prompt),docs/server/configuration.md(prompts section),docs/llms.txt,docs/llms-full.txt, and the regenerated OpenAPI spec document the versioning flow, provenance keys, and usage endpoints.