Skip to content

feat(prompts): versioning, approval provenance, and usage stats#1021

Merged
cjimti merged 1 commit into
mainfrom
feat/1009-prompt-versioning
Jul 22, 2026
Merged

feat(prompts): versioning, approval provenance, and usage stats#1021
cjimti merged 1 commit into
mainfrom
feat/1009-prompt-versioning

Conversation

@cjimti

@cjimti cjimti commented Jul 22, 2026

Copy link
Copy Markdown
Member

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_versions table (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 live prompts row 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 draft version row, and every caller keeps being served the approved snapshot until an admin approves the draft. manage_prompt update returns status: "pending_approval" with the draft's pending_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 the manage_prompt tool, the admin REST handler, and the portal REST handler. The gate is also a store-level invariant: Update and UpdateWithVersion re-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. CreateDraftVersion refuses 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/get responses stamp prompt_version, prompt_approved_by, prompt_approved_at, and prompt_reference into _meta; manage_prompt use includes version in 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/get on a database prompt, or a resolved manage_prompt use) emits a prompt_serve audit 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 get reports run_count and last_run_at, and bulk rollup endpoints back the portal library view. No new tracking pipeline.

New REST surface

Route Access Purpose
GET /api/v1/admin/prompts/{id}/versions admin Full version history, newest first, with content and author
GET /api/v1/admin/prompts/{id}/versions/{version} admin One version snapshot
POST /api/v1/admin/prompts/{id}/versions/{version}/approve admin Apply a pending draft to the live prompt
POST /api/v1/admin/prompts/{id}/versions/{version}/reject admin Reject a pending draft, live row untouched
GET /api/v1/admin/prompts/usage admin Run count / last-run per prompt id
GET /api/v1/portal/prompts/{id}/versions owner or admin Version history of an owned prompt
GET /api/v1/portal/prompts/usage any user Usage for the caller's visible (enabled) prompts only

The handlers live in the new pkg/prompt/versionhttp package 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

  • The versioning capability is a prompt.VersionStore interface 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). ApplyEdit asserts the capability from the store itself and degrades to a plain unversioned update for stores without it, so no wiring changes were needed on Platform and no new facade method was added.
  • All version writes lock the prompt row first (consistent lock order), which serializes version-number allocation; 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.
  • First approval through any path (status transition, promotion approval) stamps the prompt's current version row in the same transaction, so "approved" always names a specific snapshot regardless of which surface performed it.
  • The new REST handlers live outside pkg/admin and pkg/portal because both packages sit at the package-size budget; the portal's inline delete permission checks were consolidated into the shared checkPortalPromptPermission in the process.

Verification

  • make verify passes (full CI-equivalent suite; patch coverage 90.8% against the 80% gate, every new function at or above 80%).
  • make test-realdb passes. 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 real mcp.Server with the prompt-visibility middleware, an admin's content edit via the real manage_prompt handler leaves another user served the approved snapshot, and only approving the draft flips the served content and _meta provenance.
  • Unit tests cover the review-gate matrix (gated, mixed, metadata-only, personal, system, no-capability fallback), the store transactions via sqlmock, the REST authorization rules (owner/admin/anonymous, disabled prompts excluded from non-admin usage), and the conflict-versus-500 error mapping.

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.

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

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.87262% with 76 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.31%. Comparing base (899b882) to head (519c5cd).

Files with missing lines Patch % Lines
pkg/prompt/postgres/version.go 80.67% 20 Missing and 20 partials ⚠️
pkg/prompt/versionhttp/handler.go 94.23% 3 Missing and 3 partials ⚠️
pkg/prompt/versionhttp/usage.go 90.32% 3 Missing and 3 partials ⚠️
internal/platform/promptlayer/listchanged.go 83.33% 4 Missing ⚠️
pkg/audit/postgres/usage.go 78.94% 2 Missing and 2 partials ⚠️
pkg/prompt/postgres/store.go 91.11% 2 Missing and 2 partials ⚠️
internal/platform/promptlayer/tool.go 86.36% 3 Missing ⚠️
pkg/admin/prompt_handler.go 80.00% 2 Missing and 1 partial ⚠️
internal/platform/promptlayer/serve.go 95.65% 1 Missing and 1 partial ⚠️
pkg/platform/platform.go 50.00% 1 Missing and 1 partial ⚠️
... and 2 more
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cjimti
cjimti merged commit 2142297 into main Jul 22, 2026
10 checks passed
@cjimti
cjimti deleted the feat/1009-prompt-versioning branch July 22, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prompts: versioning, approval provenance, and usage stats

1 participant