Skip to content

feat: add expiry field to virtual keys - #4887

Merged
akshaydeo merged 1 commit into
devfrom
07-03-feat_add_expiration_support_to_virtual_keys
Jul 3, 2026
Merged

feat: add expiry field to virtual keys#4887
akshaydeo merged 1 commit into
devfrom
07-03-feat_add_expiration_support_to_virtual_keys

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

This PR adds optional expiry support for virtual keys. A virtual key can now be given an expires_at timestamp; once that time passes, all requests using the key are rejected with a 403. Keys with no expiry continue to work indefinitely as before.

Changes

  • Added ExpiresAt *time.Time field to TableVirtualKey with a corresponding database migration (add_virtual_key_expires_at_column) that adds a nullable expires_at column to governance_virtual_keys.
  • Added IsExpiredAt(now time.Time) bool method on TableVirtualKey that treats now >= expires_at as expired and nil as never-expires.
  • Included ExpiresAt in the GenerateVirtualKeyHash function (only when set, so existing rows without an expiry retain their current hash).
  • Enforced expiry checks in PreRequestHook, PreMCPHook, and EvaluateVirtualKeyRequest in the governance plugin and resolver, returning a 403 with a clear "Virtual key has expired" message.
  • Expanded UpdateVirtualKey to persist the expires_at column. Sending a timestamp sets a new expiry; sending "" clears it; omitting the field leaves it unchanged.
  • Added expires_at validation in the HTTP handler for both create (must be a future timestamp) and update (RFC3339 parse + future check).
  • Updated the JSON config schema to document the expires_at field.
  • Added an ExpiryPickerField component in the virtual key form with "Never", preset buttons (30 min, 1 h, 24 h, 7 days), and a custom date-time picker. The form only sends the expiry field when it has been explicitly changed, preventing an already-expired key's old timestamp from being re-submitted and rejected during an unrelated edit.
  • Updated the virtual keys table and detail sheet to show an "Expired" badge and display the expiry time with a human-readable relative label.
  • Extended the DateTimePicker component to accept a buttonVariant prop so the expiry picker can reflect selection state visually.
  • Added tests covering hash stability for keys without expiry and hash differentiation for keys with different expiry timestamps.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

# Core/Transports
go test ./...

# UI
cd ui
pnpm i
pnpm build

Create a key with an expiry:

POST /governance/virtual-keys
{ "name": "expiring-key", "expires_at": "2025-12-31T23:59:59Z", ... }

Requests using this key after the expiry timestamp should receive a 403 with "Virtual key has expired".

Clear an expiry:

PATCH /governance/virtual-keys/{id}
{ "expires_at": "" }

The key should now work indefinitely.

Verify hash stability: A virtual key with no ExpiresAt must produce the same hash before and after this migration is applied.

Screenshots/Recordings

The virtual key form now includes an expiry picker row with preset buttons and a custom date-time picker. The keys table and detail sheet show an "Expired" badge with a relative time label when a key has passed its expiry.

Breaking changes

  • Yes
  • No

Related issues

Security considerations

Expired keys are rejected at the earliest enforcement point in both the HTTP request path (PreRequestHook) and the MCP execution path (PreMCPHook), failing closed. The expiry is checked in-memory against the already-loaded virtual key record, so no additional database queries are introduced on the hot path.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown
Collaborator Author

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 692beed1-fb35-4aaf-b1ed-0197daf9f9c1

📥 Commits

Reviewing files that changed from the base of the PR and between 21eac0c and 041a823.

📒 Files selected for processing (18)
  • framework/configstore/clientconfig.go
  • framework/configstore/migrations.go
  • framework/configstore/rdb.go
  • framework/configstore/tables/virtualkey.go
  • plugins/governance/main.go
  • plugins/governance/resolver.go
  • plugins/governance/resolver_test.go
  • tests/cmd/e2eseed/go.mod
  • tests/cmd/seed/go.mod
  • tests/cmd/seedvks/go.mod
  • transports/bifrost-http/handlers/governance.go
  • transports/bifrost-http/lib/config_test.go
  • transports/config.schema.json
  • ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx
  • ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
  • ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx
  • ui/components/ui/datePickerWithRange.tsx
  • ui/lib/types/governance.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/cmd/seedvks/go.mod
  • tests/cmd/seed/go.mod
🚧 Files skipped from review as they are similar to previous changes (15)
  • transports/config.schema.json
  • tests/cmd/e2eseed/go.mod
  • framework/configstore/clientconfig.go
  • ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx
  • framework/configstore/rdb.go
  • ui/components/ui/datePickerWithRange.tsx
  • transports/bifrost-http/lib/config_test.go
  • ui/lib/types/governance.ts
  • framework/configstore/migrations.go
  • framework/configstore/tables/virtualkey.go
  • plugins/governance/resolver.go
  • ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx
  • plugins/governance/main.go
  • transports/bifrost-http/handlers/governance.go
  • ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Virtual keys can now be configured with an optional expires_at timestamp to automatically expire at/after the specified time.
    • UI now displays expiry status, including an “Expires” row with relative time and exact timestamp.
    • CSV export and status badges now show “Expired” with correct precedence.
  • Bug Fixes
    • Expiry values are persisted correctly on create/update, and expired keys are blocked from request routing and tool execution (with clearer inactive vs expired responses).
  • Documentation
    • Governance config schema and API behavior now document the expires_at field and how to clear vs omit it.

Walkthrough

This PR adds virtual key expiry support across storage, governance enforcement, HTTP request handling, schema/types, and UI creation and display flows.

Changes

Virtual Key Expiry

Layer / File(s) Summary
Data model, migration, and hash generation
framework/configstore/tables/virtualkey.go, framework/configstore/migrations.go, framework/configstore/rdb.go, framework/configstore/clientconfig.go
Adds ExpiresAt field and IsExpiredAt helper, an idempotent migration for expires_at, update persistence for the new column, and hash generation that includes ExpiresAt when set.
Expiry enforcement in governance plugin
plugins/governance/resolver.go, plugins/governance/main.go, plugins/governance/resolver_test.go
Blocks expired virtual keys in request and MCP gating, separates inactive vs expired failures, and adds resolver coverage for allow/block cases.
HTTP API create/update validation and schema
transports/bifrost-http/handlers/governance.go, transports/config.schema.json, transports/bifrost-http/lib/config_test.go
Adds expires_at to create/update requests, validates future timestamps and clear semantics, updates schema docs, and extends hash tests.
UI create/edit form with expiry picker
ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx, ui/components/ui/datePickerWithRange.tsx, ui/lib/types/governance.ts
Adds an expiry picker, wires expiry into form defaults and submission payloads, adds buttonVariant support to DateTimePicker, and updates governance types.
UI status display and table badges
ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx, ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx
Shows expiry status and timestamp in the details sheet and updates table status rendering and CSV export precedence for expired keys.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • maximhq/bifrost#4665: Both PRs modify GenerateVirtualKeyHash in framework/configstore/clientconfig.go, changing how virtual key hashes are computed.

Suggested reviewers: danpiths, roroghost17, akshaydeo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding virtual-key expiry support.
Description check ✅ Passed The description follows the template well and covers summary, changes, testing, screenshots, risks, and checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 07-03-feat_add_expiration_support_to_virtual_keys

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"

🔧 ast-grep (0.44.0)
transports/bifrost-http/lib/config_test.go

ast-grep timed out on this file


Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from roroghost17 July 3, 2026 12:11
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

The change is safe to merge: expiry enforcement is fail-closed across all execution paths, the migration is backward-compatible, and hash stability for existing rows is preserved.

Expiry is enforced in three independent code paths (PreRequestHook, PreMCPHook, EvaluateVirtualKeyRequest), each failing closed. The migration adds a nullable column with no default, avoiding table locks. The hash function only adds ExpiresAt when non-nil, so existing rows retain their current hashes. GORM's explicit Select list ensures nil ExpiresAt writes NULL when clearing. The UTC↔datetime-local conversion in the UI is correct, and the dirty-field guard correctly prevents re-submitting an already-expired timestamp on unrelated edits.

No files require special attention.

Important Files Changed

Filename Overview
framework/configstore/tables/virtualkey.go Adds ExpiresAt *time.Time field and IsExpiredAt method; nil-safe, boundary semantics (now >= expires_at = expired) correct
framework/configstore/clientconfig.go Adds ExpiresAt to GenerateVirtualKeyHash only when non-nil, preserving hash stability for all existing rows without expiry
framework/configstore/migrations.go Adds nullable expires_at column via addColumnIfNotExists; no index (in-memory check), rollback defined, safe for large tables since column is nullable with no default
framework/configstore/rdb.go Adds expires_at to the explicit Select list in UpdateVirtualKey, allowing GORM to write NULL when ExpiresAt is nil (clearing the expiry)
plugins/governance/main.go Expiry check added to PreRequestHook (early-return pattern) and PreMCPHook (explicit 403 short-circuit, matching the existing inactive-key structure)
plugins/governance/resolver.go Expiry check added to EvaluateVirtualKeyRequest after the inactive check, returning DecisionVirtualKeyBlocked with a clear reason
plugins/governance/resolver_test.go Adds four tests covering: no-expiry allow, future-expiry allow, past-expiry block, and inactive-with-future-expiry block
transports/bifrost-http/handlers/governance.go Create handler uses *time.Time with future-only validation; update handler uses *string with empty-string-clear semantic, RFC3339 parse, and future validation before fetching the VK
ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx ExpiryPickerField with presets and DateTimePicker; UTC↔datetime-local conversion is correct; dirty-field guard prevents re-sending old expired timestamp on unrelated edits
ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx Adds client-side expired badge; replaces the active toggle with an Expired badge when the key has passed its expiry and is still marked active
ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx Shows Expired badge in status row and a relative-time expiry row using date-fns formatDistanceToNow
ui/components/ui/datePickerWithRange.tsx Adds optional buttonVariant prop to DateTimePicker; existing disabledBefore/useEffect sync already present, no logic changes
transports/config.schema.json Adds expires_at string/date-time field with correct description aligned to the Go handler behavior

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant PreRequestHook
    participant PreMCPHook
    participant EvaluateVirtualKeyRequest

    Client->>PreRequestHook: HTTP request with VK header
    PreRequestHook->>PreRequestHook: GetVirtualKey(value)
    alt VK nil / inactive / IsExpiredAt(now)
        PreRequestHook-->>Client: return nil (early exit, no governance stamp)
    else VK valid
        PreRequestHook->>PreRequestHook: stampGovernanceCtxFromVK
    end

    Client->>EvaluateVirtualKeyRequest: PreLLMHook → EvaluateGovernanceRequest
    EvaluateVirtualKeyRequest->>EvaluateVirtualKeyRequest: IsActiveValue()?
    alt inactive
        EvaluateVirtualKeyRequest-->>Client: 403 Virtual key is inactive
    end
    EvaluateVirtualKeyRequest->>EvaluateVirtualKeyRequest: IsExpiredAt(now)?
    alt expired
        EvaluateVirtualKeyRequest-->>Client: 403 Virtual key has expired
    end
    EvaluateVirtualKeyRequest-->>Client: DecisionAllow

    Client->>PreMCPHook: MCP tool execution
    PreMCPHook->>PreMCPHook: GetVirtualKey(value)
    alt VK nil
        PreMCPHook-->>Client: 403 VK became invalid
    else inactive
        PreMCPHook-->>Client: 403 Virtual key is inactive
    else IsExpiredAt(now)
        PreMCPHook-->>Client: 403 Virtual key has expired
    else valid
        PreMCPHook-->>Client: allow tool execution
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant PreRequestHook
    participant PreMCPHook
    participant EvaluateVirtualKeyRequest

    Client->>PreRequestHook: HTTP request with VK header
    PreRequestHook->>PreRequestHook: GetVirtualKey(value)
    alt VK nil / inactive / IsExpiredAt(now)
        PreRequestHook-->>Client: return nil (early exit, no governance stamp)
    else VK valid
        PreRequestHook->>PreRequestHook: stampGovernanceCtxFromVK
    end

    Client->>EvaluateVirtualKeyRequest: PreLLMHook → EvaluateGovernanceRequest
    EvaluateVirtualKeyRequest->>EvaluateVirtualKeyRequest: IsActiveValue()?
    alt inactive
        EvaluateVirtualKeyRequest-->>Client: 403 Virtual key is inactive
    end
    EvaluateVirtualKeyRequest->>EvaluateVirtualKeyRequest: IsExpiredAt(now)?
    alt expired
        EvaluateVirtualKeyRequest-->>Client: 403 Virtual key has expired
    end
    EvaluateVirtualKeyRequest-->>Client: DecisionAllow

    Client->>PreMCPHook: MCP tool execution
    PreMCPHook->>PreMCPHook: GetVirtualKey(value)
    alt VK nil
        PreMCPHook-->>Client: 403 VK became invalid
    else inactive
        PreMCPHook-->>Client: 403 Virtual key is inactive
    else IsExpiredAt(now)
        PreMCPHook-->>Client: 403 Virtual key has expired
    else valid
        PreMCPHook-->>Client: allow tool execution
    end
Loading

Reviews (2): Last reviewed commit: "feat: add expiration support to virtual ..." | Re-trigger Greptile

Comment thread transports/bifrost-http/handlers/governance.go
Comment thread plugins/governance/main.go
Comment thread transports/bifrost-http/lib/config_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
plugins/governance/main.go (1)

1443-1473: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the fail-closed short-circuit blocks into a helper.

The not-found, inactive, and now expired blocks in PreMCPHook are structurally identical (set governanceRejectedContextKey, build the same MCPPluginShortCircuit/BifrostError shape, differing only in Decision/message). Extracting a small helper (e.g. rejectMCPVK(ctx, decision, message)) would reduce triplication and keep future additions consistent.

♻️ Proposed helper extraction
+func (p *GovernancePlugin) rejectMCPVK(ctx *schemas.BifrostContext, decision Decision, message string) *schemas.MCPPluginShortCircuit {
+	ctx.SetValue(governanceRejectedContextKey, true)
+	return &schemas.MCPPluginShortCircuit{Error: &schemas.BifrostError{
+		Type:       bifrost.Ptr(string(decision)),
+		StatusCode: bifrost.Ptr(403),
+		Error: &schemas.ErrorField{
+			Message: message,
+		},
+	}}
+}
+
 		vk, ok := p.store.GetVirtualKey(ctx, virtualKeyValue)
 		if !ok || vk == nil {
-			// VK became invalid after initial check - fail closed for security
-			ctx.SetValue(governanceRejectedContextKey, true)
-			return req, &schemas.MCPPluginShortCircuit{Error: &schemas.BifrostError{
-				Type:       bifrost.Ptr(string(DecisionVirtualKeyNotFound)),
-				StatusCode: bifrost.Ptr(403),
-				Error: &schemas.ErrorField{
-					Message: "Virtual key not found",
-				},
-			}}, nil
+			// VK became invalid after initial check - fail closed for security
+			return req, p.rejectMCPVK(ctx, DecisionVirtualKeyNotFound, "Virtual key not found"), nil
 		}
 		if !vk.IsActiveValue() {
-			ctx.SetValue(governanceRejectedContextKey, true)
-			return req, &schemas.MCPPluginShortCircuit{Error: &schemas.BifrostError{
-				Type:       bifrost.Ptr(string(DecisionVirtualKeyBlocked)),
-				StatusCode: bifrost.Ptr(403),
-				Error: &schemas.ErrorField{
-					Message: "Virtual key is inactive",
-				},
-			}}, nil
+			return req, p.rejectMCPVK(ctx, DecisionVirtualKeyBlocked, "Virtual key is inactive"), nil
 		}
 		if vk.IsExpiredAt(time.Now().UTC()) {
-			ctx.SetValue(governanceRejectedContextKey, true)
-			return req, &schemas.MCPPluginShortCircuit{Error: &schemas.BifrostError{
-				Type:       bifrost.Ptr(string(DecisionVirtualKeyBlocked)),
-				StatusCode: bifrost.Ptr(403),
-				Error: &schemas.ErrorField{
-					Message: "Virtual key has expired",
-				},
-			}}, nil
+			return req, p.rejectMCPVK(ctx, DecisionVirtualKeyBlocked, "Virtual key has expired"), nil
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/governance/main.go` around lines 1443 - 1473, The fail-closed
short-circuit handling in PreMCPHook is duplicated across the not-found,
inactive, and expired virtual key checks. Extract the repeated
ctx.SetValue(governanceRejectedContextKey, true) plus
MCPPluginShortCircuit/BifrostError construction into a small helper such as
rejectMCPVK, and pass in the decision type and error message so all three
branches reuse the same path. Keep the existing behavior intact while making
future governance rejections consistent and easier to extend.
ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx (1)

83-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate isExpired computation — extract a shared helper.

const isExpired = !!vk.expires_at && Date.now() >= new Date(vk.expires_at).getTime(); is duplicated verbatim in the CSV export (line 83), the row-render logic (line 849), and again in virtualKeyDetailsSheet.tsx (line 140). A single isVirtualKeyExpired(expiresAt?: string | null): boolean helper (e.g. in ui/lib/utils) would keep this consistent if the expiry semantics ever change.

♻️ Suggested helper
export function isVirtualKeyExpired(expiresAt?: string | null): boolean {
	return !!expiresAt && Date.now() >= new Date(expiresAt).getTime();
}

As per path instructions, "reuse existing constants, page structure, shared components... before introducing one-off UI conventions."

Also applies to: 849-850

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx` around lines 83 -
84, The expiry check is duplicated across the virtual keys table and details
sheet, so extract it into a shared helper and reuse it everywhere. Add a
reusable isVirtualKeyExpired(expiresAt?: string | null) utility in the shared
utils area (for example ui/lib/utils), then replace the inline Date.now/new Date
logic in virtualKeysTable.tsx and virtualKeyDetailsSheet.tsx with that helper in
both the CSV export and row-render paths. Keep the existing status logic in
VirtualKeysTable and the details sheet behavior unchanged except for calling the
shared helper.

Source: Path instructions

transports/bifrost-http/handlers/governance.go (1)

168-169: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

expires_at type asymmetry causes inconsistent error UX between create and update.

CreateVirtualKeyRequest.ExpiresAt is a *time.Time, so malformed date strings fail during the top-level json.Unmarshal (line 1243) and only ever surface as the generic "Invalid JSON" error. UpdateVirtualKeyRequest.ExpiresAt is a *string that is explicitly parsed and yields the much more actionable "expires_at must be an RFC3339 timestamp" message. A client sending a bad expires_at on create gets no hint about which field is wrong.

♻️ Suggested fix: mirror the update path's explicit-string-parse pattern on create
-	ExpiresAt       *time.Time              `json:"expires_at,omitempty"`       // Optional expiry; nil means never expires
+	ExpiresAt       *string                 `json:"expires_at,omitempty"`       // Optional expiry (RFC3339); nil means never expires

Then parse it the same way updateVirtualKey does, converting to *time.Time before persisting.

Also applies to: 1276-1283

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transports/bifrost-http/handlers/governance.go` around lines 168 - 169,
CreateVirtualKeyRequest.ExpiresAt is using *time.Time, which makes malformed
expires_at values fail as generic “Invalid JSON” during json.Unmarshal instead
of a field-specific message. Change the create flow to mirror
UpdateVirtualKeyRequest and updateVirtualKey by using an explicit string field
for expires_at in the request struct, then parse it with the same RFC3339
validation and convert to *time.Time before persistence. Keep the change aligned
in governance.go for both the request type and the create handler so bad input
returns the same actionable error UX as update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@plugins/governance/main.go`:
- Around line 1443-1473: The fail-closed short-circuit handling in PreMCPHook is
duplicated across the not-found, inactive, and expired virtual key checks.
Extract the repeated ctx.SetValue(governanceRejectedContextKey, true) plus
MCPPluginShortCircuit/BifrostError construction into a small helper such as
rejectMCPVK, and pass in the decision type and error message so all three
branches reuse the same path. Keep the existing behavior intact while making
future governance rejections consistent and easier to extend.

In `@transports/bifrost-http/handlers/governance.go`:
- Around line 168-169: CreateVirtualKeyRequest.ExpiresAt is using *time.Time,
which makes malformed expires_at values fail as generic “Invalid JSON” during
json.Unmarshal instead of a field-specific message. Change the create flow to
mirror UpdateVirtualKeyRequest and updateVirtualKey by using an explicit string
field for expires_at in the request struct, then parse it with the same RFC3339
validation and convert to *time.Time before persistence. Keep the change aligned
in governance.go for both the request type and the create handler so bad input
returns the same actionable error UX as update.

In `@ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx`:
- Around line 83-84: The expiry check is duplicated across the virtual keys
table and details sheet, so extract it into a shared helper and reuse it
everywhere. Add a reusable isVirtualKeyExpired(expiresAt?: string | null)
utility in the shared utils area (for example ui/lib/utils), then replace the
inline Date.now/new Date logic in virtualKeysTable.tsx and
virtualKeyDetailsSheet.tsx with that helper in both the CSV export and
row-render paths. Keep the existing status logic in VirtualKeysTable and the
details sheet behavior unchanged except for calling the shared helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cf86fcd9-9fb6-4f36-b90b-11a6dc074b72

📥 Commits

Reviewing files that changed from the base of the PR and between f6dd217 and 21eac0c.

📒 Files selected for processing (17)
  • framework/configstore/clientconfig.go
  • framework/configstore/migrations.go
  • framework/configstore/rdb.go
  • framework/configstore/tables/virtualkey.go
  • plugins/governance/main.go
  • plugins/governance/resolver.go
  • tests/cmd/e2eseed/go.mod
  • tests/cmd/seed/go.mod
  • tests/cmd/seedvks/go.mod
  • transports/bifrost-http/handlers/governance.go
  • transports/bifrost-http/lib/config_test.go
  • transports/config.schema.json
  • ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx
  • ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
  • ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx
  • ui/components/ui/datePickerWithRange.tsx
  • ui/lib/types/governance.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 3, 2026
@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 07-03-feat_add_expiration_support_to_virtual_keys branch from 21eac0c to 041a823 Compare July 3, 2026 14:53

akshaydeo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jul 3, 3:16 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 3, 3:17 PM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit 9e89466 into dev Jul 3, 2026
14 of 15 checks passed
@akshaydeo
akshaydeo deleted the 07-03-feat_add_expiration_support_to_virtual_keys branch July 3, 2026 15:17
yangtuooc added a commit to yangtuooc/bifrost that referenced this pull request Jul 4, 2026
* 'dev' of https://github.com/maximhq/bifrost:
  ipv6 support (maximhq#4895)
  docs: add virtual key expiry support docs (maximhq#4889)
  test: add Postman e2e collection and runner for virtual key expiry validation and enforcement (maximhq#4888)
  feat: add expiry field to virtual keys (maximhq#4887)
  fix: converts thinking to disabled if tool choice is required for deepseek (maximhq#4861)
  chore: adds docs for deepseek provider (maximhq#4854)
  chore: adds tests for deepseek provider (maximhq#4853)
  feat: adds deepseek provider (maximhq#4852)
  fix: cost for image generation or image edit streaming (maximhq#4802)
  feat: add `BedrockMantleKeyConfig` support to key hashing, schema/table mapping, and sensitive field clearing (maximhq#4886)
  fix: skip O(N) reference refresh on request-time rate-limit/budget reset (maximhq#4883)
  refactor: simplify Responses lifecycle permissions to require explicit per-verb flags and expose them in UI (maximhq#4880)
  fix: append datasheet models for incomplete list models call (maximhq#4879)

# Conflicts:
#	ui/app/workspace/providers/fragments/allowedRequestsFields.tsx
#	ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx
#	ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
#	ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx
#	ui/components/ui/datePickerWithRange.tsx
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.

3 participants