Skip to content

feat: add expiration support to virtual keys - #4882

Closed
Pratham-Mishra04 wants to merge 1 commit into
devfrom
feat-adds_vk_expiration_support
Closed

feat: add expiration support to virtual keys#4882
Pratham-Mishra04 wants to merge 1 commit into
devfrom
feat-adds_vk_expiration_support

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

Adds optional expiry support for virtual keys. Operators can now set an expires_at timestamp on a virtual key; once that time passes, the key is automatically treated as blocked without needing to manually deactivate it.

Changes

  • Added expires_at nullable timestamp column to governance_virtual_keys via a new database migration, with rollback support.
  • Added IsExpiredAt(now time.Time) bool method on TableVirtualKey for in-memory expiry checks (no DB index needed since expiry is evaluated from the already-loaded key).
  • Updated UpdateVirtualKey to include expires_at in the set of persisted fields.
  • Updated EvaluateVirtualKeyRequest in the budget resolver to block expired keys with a DecisionVirtualKeyBlocked result.
  • Updated PreRequestHook to reject expired keys at the pre-request stage.
  • Updated PreMCPHook to separately check IsActiveValue() and IsExpiredAt(), returning distinct 403 error messages for inactive vs. expired keys.
  • Added ExpiresAt to CreateVirtualKeyRequest and UpdateVirtualKeyRequest HTTP handler types, with validation that the timestamp must be in the future.
  • Added ClearExpiresAt boolean to UpdateVirtualKeyRequest to explicitly remove an existing expiry, mutually exclusive with setting a new expires_at.
  • UI: Added ExpiryPickerField component with quick-select presets (30 min, 1 hour, 24 hours, 7 days) and a custom date-time picker.
  • UI: Expiry changes are only sent to the backend when the field is actually dirty, preventing re-submission of an already-expired timestamp when editing other fields.
  • UI: Virtual keys table and detail sheet now display an "Expired" badge and expiry timestamp with a human-readable relative time.
  • UI: CSV export includes "Expired" as a possible status value.

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
  1. Create a virtual key with expires_at set to a time 1 minute in the future. Confirm requests succeed before expiry.
  2. Wait for the key to expire. Confirm subsequent requests return a 403 with "Virtual key has expired".
  3. Create a virtual key with expires_at in the past — confirm the API returns a 400 validation error.
  4. Update an existing virtual key with clear_expires_at: true and confirm the expiry is removed.
  5. Attempt to set both expires_at and clear_expires_at: true in the same update request — confirm a 400 error is returned.
  6. In the UI, open the virtual key form and verify the expiry picker presets and custom date-time picker work correctly.
  7. Verify that editing an already-expired key's other fields does not cause the backend to reject the request due to the expired timestamp being re-sent.

Screenshots/Recordings

Add before/after screenshots of the virtual key table and detail sheet showing the "Expired" badge and expiry field.

Breaking changes

  • Yes
  • No

Related issues

Security considerations

Expired keys are rejected at both the pre-request and pre-MCP hook stages, ensuring fail-closed behavior. Expiry is evaluated in-memory from the loaded key object, so there is no risk of a time-of-check/time-of-use gap introduced by additional DB queries. The clear_expires_at and expires_at fields are mutually exclusive to prevent ambiguous update semantics.

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

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Virtual keys can now be given an expiration date when created or updated, with an option to remove an existing expiration.
    • Virtual key details and tables now show expiry information and status updates such as Active, Inactive, Expired, or Exhausted.
  • Bug Fixes

    • Expired virtual keys are now correctly blocked in request handling and validation flows.
    • Status exports and on-screen indicators now reflect expiration accurately.

Walkthrough

Adds virtual key expiration support end-to-end: a new database migration and expires_at column, an IsExpiredAt model method, governance plugin enforcement in PreRequestHook/PreMCPHook/resolver, HTTP API create/update validation with clear_expires_at, and UI expiry picker and status displays.

Changes

Virtual Key Expiry

Layer / File(s) Summary
Schema, migration, and expiry model
framework/configstore/migrations.go, framework/configstore/tables/virtualkey.go, framework/configstore/rdb.go
Adds add_virtual_key_expires_at_column migration with rollback, adds nullable ExpiresAt field and IsExpiredAt method to TableVirtualKey, and includes expires_at in UpdateVirtualKey's update column list.
Governance enforcement of expiry
plugins/governance/main.go, plugins/governance/resolver.go
Updates PreRequestHook, PreMCPHook, and EvaluateVirtualKeyRequest to check IsExpiredAt, blocking or short-circuiting expired/inactive virtual keys with distinct decisions and messages.
HTTP API create/update expiry validation
transports/bifrost-http/handlers/governance.go
Adds expires_at to create requests and expires_at/clear_expires_at to update requests, validating future timestamps and mutual exclusivity, and persists changes during create/update transactions.
UI types for expiry
ui/lib/types/governance.ts
Updates VirtualKey, CreateVirtualKeyRequest, and UpdateVirtualKeyRequest types to support nullable expires_at and clear_expires_at.
Expiry picker in create/edit sheet
ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
Adds an ExpiryPickerField component with presets/clear/never, form schema field, default value mapping, and submit logic sending expires_at or clear_expires_at based on dirty state.
Expiry status display in details and table
ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx, ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx
Computes expiration status to show "Expired" badges, an "Expires" row, and updated CSV export/status precedence, hiding the active switch for expired keys.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GovernancePlugin
  participant ConfigStore
  Client->>GovernancePlugin: PreRequestHook / PreMCPHook
  GovernancePlugin->>ConfigStore: fetch virtual key
  ConfigStore-->>GovernancePlugin: virtual key record
  GovernancePlugin->>GovernancePlugin: IsExpiredAt(now)
  GovernancePlugin-->>Client: DecisionVirtualKeyBlocked (403, "Virtual key has expired")
Loading
sequenceDiagram
  participant User
  participant VirtualKeySheet
  participant ExpiryPickerField
  participant GovernanceAPI
  User->>ExpiryPickerField: pick date or clear
  ExpiryPickerField->>VirtualKeySheet: update expiresAt field
  User->>VirtualKeySheet: submit form
  VirtualKeySheet->>VirtualKeySheet: check dirtyFields.expiresAt
  VirtualKeySheet->>GovernanceAPI: send expires_at or clear_expires_at
Loading

Possibly related PRs

  • maximhq/bifrost#3452: Both PRs modify framework/configstore/rdb.go's UpdateVirtualKey DB persistence logic by changing the update column list on the same code path.

Suggested reviewers: danpiths, roroghost17, akshaydeo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding expiration support for virtual keys.
Description check ✅ Passed The description follows the template well and includes summary, changes, testing, security, and change-type details.
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 feat-adds_vk_expiration_support

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"


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

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

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 3/5

Safe to merge only after restoring the IsActiveValue() guard in PreRequestHook; everything else is well-structured.

The core expiry enforcement in EvaluateVirtualKeyRequest and PreMCPHook is correct and fail-closed. However, PreRequestHook inadvertently drops the !virtualKey.IsActiveValue() early-return that previously caused inactive VKs to skip all routing work. Those VKs now proceed through context stamping, routing-rule evaluation, load-balancer counter updates, and MCP tool-allowlist computation before being blocked in PreLLMHook. The requests are still ultimately blocked, but the routing side-effects are unintended. No tests cover the new expiry path in the resolver or plugin, which makes regression risk harder to bound for a security-sensitive feature.

plugins/governance/main.go — the PreRequestHook early-return condition needs !virtualKey.IsActiveValue() restored alongside the new IsExpiredAt check.

Important Files Changed

Filename Overview
framework/configstore/migrations.go Adds migrationAddVirtualKeyExpiresAtColumn using addColumnIfNotExists on a nullable column — no index, minimal lock, safe rollback via dropColumnIfExists.
framework/configstore/rdb.go Adds expires_at to the Select column list in UpdateVirtualKey so the field is persisted; straightforward and correct.
framework/configstore/tables/virtualkey.go Adds nullable ExpiresAt *time.Time field and IsExpiredAt(now time.Time) bool method; nil-safe, UTC-normalized, and semantically correct.
plugins/governance/main.go PreMCPHook gains correct separate inactive/expired checks; PreRequestHook incorrectly drops the IsActiveValue() early-return guard, letting inactive (non-expired) VKs proceed through routing and load-balancing before being blocked downstream.
plugins/governance/resolver.go Adds IsExpiredAt check in EvaluateVirtualKeyRequest after IsActiveValue, returning DecisionVirtualKeyBlocked; correct placement and ordering.
transports/bifrost-http/handlers/governance.go Adds ExpiresAt/ClearExpiresAt to create/update handlers with correct mutual-exclusion check and future-timestamp validation.
ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx ExpiryPickerField and dirty-field expiry payload logic are well-designed; timezone conversion for datetime-local input is correct.
ui/app/workspace/virtual-keys/views/virtualKeyDetailsSheet.tsx Adds expired badge and expiry timestamp display; isExpired computed at render time with Date.now() so the badge can lag reality if the page is left open.
ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx Status column replaces the active toggle with an Expired badge for expired active keys; same render-time Date.now() concern as the detail sheet.
ui/lib/types/governance.ts Adds expires_at?: string

Sequence Diagram

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

    Client->>PreRequestHook: request + x-bf-vk header
    PreRequestHook->>PreRequestHook: GetVirtualKey()
    alt VK not found / nil / IsExpiredAt
        PreRequestHook-->>Client: return nil
    else IsActiveValue false - REGRESSION not checked
        Note over PreRequestHook: Falls through to routing
        PreRequestHook->>PreRequestHook: stampGovernanceCtx, routing, loadBalance
    else Valid key
        PreRequestHook->>PreRequestHook: stampGovernanceCtx, routing, loadBalance
    end
    Client->>PreLLMHook: routed request
    PreLLMHook->>Resolver: EvaluateVirtualKeyRequest
    Resolver->>Resolver: IsActiveValue check
    Resolver->>Resolver: IsExpiredAt check
    alt Blocked
        Resolver-->>Client: 403
    else Allowed
        Resolver-->>Client: proceed
    end
    Client->>PreMCPHook: MCP tool request
    PreMCPHook->>PreMCPHook: GetVirtualKey()
    alt not found or nil
        PreMCPHook-->>Client: 403
    else inactive
        PreMCPHook-->>Client: 403 inactive
    else expired
        PreMCPHook-->>Client: 403 expired
    else valid
        PreMCPHook-->>Client: proceed
    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 PreLLMHook as PreLLMHook
    participant PreMCPHook
    participant Resolver as EvaluateVirtualKeyRequest

    Client->>PreRequestHook: request + x-bf-vk header
    PreRequestHook->>PreRequestHook: GetVirtualKey()
    alt VK not found / nil / IsExpiredAt
        PreRequestHook-->>Client: return nil
    else IsActiveValue false - REGRESSION not checked
        Note over PreRequestHook: Falls through to routing
        PreRequestHook->>PreRequestHook: stampGovernanceCtx, routing, loadBalance
    else Valid key
        PreRequestHook->>PreRequestHook: stampGovernanceCtx, routing, loadBalance
    end
    Client->>PreLLMHook: routed request
    PreLLMHook->>Resolver: EvaluateVirtualKeyRequest
    Resolver->>Resolver: IsActiveValue check
    Resolver->>Resolver: IsExpiredAt check
    alt Blocked
        Resolver-->>Client: 403
    else Allowed
        Resolver-->>Client: proceed
    end
    Client->>PreMCPHook: MCP tool request
    PreMCPHook->>PreMCPHook: GetVirtualKey()
    alt not found or nil
        PreMCPHook-->>Client: 403
    else inactive
        PreMCPHook-->>Client: 403 inactive
    else expired
        PreMCPHook-->>Client: 403 expired
    else valid
        PreMCPHook-->>Client: proceed
    end
Loading

Reviews (1): Last reviewed commit: "fix(governance): support expiring virtua..." | Re-trigger Greptile

Comment on lines +1207 to 1209
if !ok || virtualKey == nil || virtualKey.IsExpiredAt(time.Now().UTC()) {
return nil
}

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.

P1 Inactive VK check dropped from PreRequestHook short-circuit

The condition previously returned early for inactive keys (!virtualKey.IsActiveValue()); this PR replaces that with the expiry check but doesn't preserve the inactive check. As a result, inactive (but not expired) virtual keys now fall through the guard and enter the routing pipeline — stampGovernanceCtxFromVK, routing-rule evaluation, loadBalanceProvider, and MCP tool-allowlist computation all execute before PreLLMHook eventually blocks the key via EvaluateVirtualKeyRequest. Load-balancer counters and MCP context values can be mutated for requests that will never be served. Both checks should be in the condition:

if !ok || virtualKey == nil || !virtualKey.IsActiveValue() || virtualKey.IsExpiredAt(time.Now().UTC()) {
    return nil
}

Comment on lines 847 to 848
virtualKeys.map((vk) => {
const isRevealed = revealedKeys.has(vk.id);

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.

P2 Expiry status computed once at render; won't update while page is open

Both virtualKeysTable.tsx and virtualKeyDetailsSheet.tsx compute isExpired with Date.now() during the render pass. A key that expires minutes after the table loads will continue showing "Active" until the component re-renders (e.g. navigation or manual refresh). Consider either a short polling interval or a useMemo/useEffect pair that schedules a state flip at new Date(vk.expires_at).getTime() - Date.now() milliseconds so the badge transitions automatically.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugins/governance/main.go (1)

1203-1210: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Restore the inactive VK guard in PreRequestHook. Inactive keys now reach routing, allowlist stamping, and MCP injection before PreLLMHook rejects them. Keep this hook aligned with PreMCPHook by short-circuiting on !IsActiveValue() before any VK-scoped setup.

🤖 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 1203 - 1210, The PreRequestHook
virtual key lookup currently only filters missing or expired keys, so inactive
keys can still proceed into routing, allowlist stamping, and MCP setup. Update
the PreRequestHook logic around p.store.GetVirtualKey and the
virtualKey.IsExpiredAt check to also short-circuit when the key is not active by
using IsActiveValue(), keeping it aligned with PreMCPHook and ensuring VK-scoped
setup only happens for active keys.

Source: Path instructions

🧹 Nitpick comments (4)
ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx (2)

189-223: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add data-testid to the new Expiry picker's interactive elements.

ExpiryPickerField introduces several new interactive controls (Clear button, Never button, preset buttons, DateTimePicker) with no data-testid attributes. E2E tests will need stable selectors for these.

🏷️ Suggested testids
-				{value && (
-					<Button type="button" variant="ghost" size="sm" onClick={() => onChange(null)}>
+				{value && (
+					<Button type="button" variant="ghost" size="sm" data-testid="vk-expiry-clear-btn" onClick={() => onChange(null)}>
 						Clear
 					</Button>
 				)}
 			</div>
 			<p className="text-muted-foreground text-xs">Leave empty for a key that never expires.</p>
 			{summary && <p className="text-sm font-medium">{summary}</p>}
 			<div className="flex flex-wrap gap-1.5">
-				<Button type="button" variant={!value ? "secondary" : "outline"} size="sm" onClick={() => onChange(null)}>
+				<Button type="button" variant={!value ? "secondary" : "outline"} size="sm" data-testid="vk-expiry-never-btn" onClick={() => onChange(null)}>
 					Never
 				</Button>
 				{EXPIRY_PRESETS.map(({ label, ms }) => (
-					<Button key={label} type="button" variant="outline" size="sm" onClick={() => onChange(presetFromNow(ms))}>
+					<Button key={label} type="button" variant="outline" size="sm" data-testid={`vk-expiry-preset-${label}`} onClick={() => onChange(presetFromNow(ms))}>
 						{label}
 					</Button>
 				))}
 				<DateTimePicker
+					data-testid="vk-expiry-datetime-picker"
 					buttonClassName="h-8 text-sm px-3"
Based on learnings, `data-testid` conventions of `--` should be applied consistently; adjust names to match existing `vk-*` patterns in this file.
🤖 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/virtualKeySheet.tsx` around lines 189 -
223, Add stable data-testid attributes to the new interactive controls in
ExpiryPickerField so E2E tests can target them reliably. Update the Clear
button, Never button, each preset button in EXPIRY_PRESETS, and the
DateTimePicker with vk-* testid names that match the existing virtual key naming
pattern in this file. Use consistent entity-element-qualifier-style identifiers
so the selectors remain predictable even if the UI layout changes.

Source: Coding guidelines


213-218: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No client-side guard against picking a past time on the current day.

disabledBefore={new Date()} only disables prior days in the calendar; the paired TimePicker still allows selecting an already-passed time on today's date. The backend rejects this with a 400 (expires_at must be a future timestamp), so submission fails, surfaced only via a generic toast. Adding a same-day future-time check (zod .refine or inline validation) would give the user immediate, actionable feedback instead of a round-trip failure.

🤖 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/virtualKeySheet.tsx` around lines 213 -
218, The DateTimePicker flow in virtualKeySheet.tsx only blocks past days, so
users can still pick an earlier time today and hit the backend 400. Add
client-side validation in the virtual key expiration form path around
DateTimePicker/onDateTimeUpdate (or the zod schema/refine used by the sheet) to
reject any timestamp that is not strictly in the future, including same-day
times. Surface an inline validation message before submit so the user gets
immediate feedback instead of a generic toast.
ui/lib/types/governance.ts (1)

183-184: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

expires_at on UpdateVirtualKeyRequest shouldn't allow null.

Unlike team_id/customer_id (typed as schemas.OptionalJSON[string] on the backend to distinguish "not provided" from "explicit null/clear"), the backend ExpiresAt field is a plain *time.Time with omitempty: ExpiresAt *time.Time json:"expires_at,omitempty" // Set a new expiry; nil means "leave unchanged". During JSON unmarshaling, sending expires_at: null is indistinguishable from omitting the field entirely — both decode to a nil pointer, meaning "leave unchanged", not "clear". Clearing is only achievable via clear_expires_at: true. Allowing string | null here invites a future caller to (incorrectly) assume null clears the expiry.

Current call sites in virtualKeySheet.tsx correctly avoid sending null (they use clear_expires_at), so this isn't exploited today — but the type should reflect the real contract.

🛠️ Suggested type tightening
-	expires_at?: string | null; // ISO 8601 UTC timestamp; omit to leave unchanged
+	expires_at?: string; // ISO 8601 UTC timestamp to set a new expiry; use clear_expires_at to remove
 	clear_expires_at?: boolean; // true to remove an existing expiry (mutually exclusive with expires_at)
🤖 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/lib/types/governance.ts` around lines 183 - 184, The
`UpdateVirtualKeyRequest` type allows `expires_at` to be `null`, but the backend
`ExpiresAt` contract only treats omission as “leave unchanged” and uses
`clear_expires_at` to clear the value. Tighten the `expires_at` field in
`governance.ts` to accept only a string timestamp (or be omitted), and keep
`clear_expires_at` as the only way to remove an expiry. Check related callers
like `virtualKeySheet.tsx` to ensure they continue using `clear_expires_at`
instead of relying on `null`.
ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx (1)

904-912: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a data-testid to the new Expired badge; replacing the active-switch testid may break E2E selectors.

When showExpiredBadge is true, the row no longer renders VKActiveSwitch (data-testid="vk-active-switch-${vk.name}"), and the new Badge has no testid of its own. Any E2E test asserting on the active-switch selector for all rows, or needing to detect the "Expired" state, has no stable hook here.

🏷️ Suggested fix
 {showExpiredBadge ? (
-	<Badge variant="destructive" className="text-xs">
+	<Badge variant="destructive" className="text-xs" data-testid={`vk-expired-badge-${vk.name}`}>
 		Expired
 	</Badge>
 ) : (
 	<VKActiveSwitch vk={vk} hasUpdateAccess={hasUpdateAccess} onToggle={handleToggleActive} />
 )}
Based on path instructions, "UI `data-testid` attributes are load-bearing for E2E tests" and removal/replacement of an existing testid requires updating `tests/e2e/` references.
🤖 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 904 -
912, The new Expired badge in virtualKeysTable should get its own stable
data-testid because it replaces VKActiveSwitch and removes the existing
vk-active-switch-${vk.name} hook for expired rows. Update the Badge branch in
the showExpiredBadge conditional to add a unique testid for the expired state,
and then adjust any affected tests in tests/e2e/ that currently rely on the
active-switch selector so they assert the correct state using the new Badge
identifier. Use VKActiveSwitch and the Badge render path as the key locations.

Source: Path instructions

🤖 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.

Inline comments:
In `@transports/bifrost-http/handlers/governance.go`:
- Line 168: The governance virtual key model now includes ExpiresAt, but the
config schema still rejects it because governance.virtual_keys[] is locked down
with additionalProperties false. Update transports/config.schema.json to
explicitly add expires_at and clear_expires_at to the virtual key object
definition, using the same naming and semantics as the governance types so
config-backed keys validate correctly.

---

Outside diff comments:
In `@plugins/governance/main.go`:
- Around line 1203-1210: The PreRequestHook virtual key lookup currently only
filters missing or expired keys, so inactive keys can still proceed into
routing, allowlist stamping, and MCP setup. Update the PreRequestHook logic
around p.store.GetVirtualKey and the virtualKey.IsExpiredAt check to also
short-circuit when the key is not active by using IsActiveValue(), keeping it
aligned with PreMCPHook and ensuring VK-scoped setup only happens for active
keys.

---

Nitpick comments:
In `@ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx`:
- Around line 189-223: Add stable data-testid attributes to the new interactive
controls in ExpiryPickerField so E2E tests can target them reliably. Update the
Clear button, Never button, each preset button in EXPIRY_PRESETS, and the
DateTimePicker with vk-* testid names that match the existing virtual key naming
pattern in this file. Use consistent entity-element-qualifier-style identifiers
so the selectors remain predictable even if the UI layout changes.
- Around line 213-218: The DateTimePicker flow in virtualKeySheet.tsx only
blocks past days, so users can still pick an earlier time today and hit the
backend 400. Add client-side validation in the virtual key expiration form path
around DateTimePicker/onDateTimeUpdate (or the zod schema/refine used by the
sheet) to reject any timestamp that is not strictly in the future, including
same-day times. Surface an inline validation message before submit so the user
gets immediate feedback instead of a generic toast.

In `@ui/app/workspace/virtual-keys/views/virtualKeysTable.tsx`:
- Around line 904-912: The new Expired badge in virtualKeysTable should get its
own stable data-testid because it replaces VKActiveSwitch and removes the
existing vk-active-switch-${vk.name} hook for expired rows. Update the Badge
branch in the showExpiredBadge conditional to add a unique testid for the
expired state, and then adjust any affected tests in tests/e2e/ that currently
rely on the active-switch selector so they assert the correct state using the
new Badge identifier. Use VKActiveSwitch and the Badge render path as the key
locations.

In `@ui/lib/types/governance.ts`:
- Around line 183-184: The `UpdateVirtualKeyRequest` type allows `expires_at` to
be `null`, but the backend `ExpiresAt` contract only treats omission as “leave
unchanged” and uses `clear_expires_at` to clear the value. Tighten the
`expires_at` field in `governance.ts` to accept only a string timestamp (or be
omitted), and keep `clear_expires_at` as the only way to remove an expiry. Check
related callers like `virtualKeySheet.tsx` to ensure they continue using
`clear_expires_at` instead of relying on `null`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de2d686b-666b-446b-a2e9-dc81bc1ee68e

📥 Commits

Reviewing files that changed from the base of the PR and between 1f7f9ee and 70041ea.

📒 Files selected for processing (10)
  • framework/configstore/migrations.go
  • framework/configstore/rdb.go
  • framework/configstore/tables/virtualkey.go
  • plugins/governance/main.go
  • plugins/governance/resolver.go
  • transports/bifrost-http/handlers/governance.go
  • 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/lib/types/governance.ts

RateLimit *CreateRateLimitRequest `json:"rate_limit,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
CalendarAligned bool `json:"calendar_aligned,omitempty"` // When true, all budgets reset at clean calendar boundaries
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Optional expiry; nil means never expires

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant handler and schema sections with line numbers.
git ls-files transports/bifrost-http/handlers/governance.go transports/config.schema.json

echo "---- governance.go ----"
sed -n '140,230p' transports/bifrost-http/handlers/governance.go

echo "---- config.schema.json search ----"
rg -n '"expires_at"|"clear_expires_at"|virtual_keys|additionalProperties' transports/config.schema.json

Repository: maximhq/bifrost

Length of output: 16596


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact virtual_key object schema section if present.
python3 - <<'PY'
from pathlib import Path
p = Path('transports/config.schema.json')
text = p.read_text()
for needle in ['"virtual_keys"', '"expires_at"', '"clear_expires_at"']:
    idx = text.find(needle)
    print(f"\n== {needle} ==")
    if idx == -1:
        print("not found")
        continue
    start = max(0, text.rfind('\n', 0, idx-500))
    end = text.find('\n', idx+1000)
    print(text[start:end if end!=-1 else len(text)])
PY

Repository: maximhq/bifrost

Length of output: 1776


Add expires_at and clear_expires_at to transports/config.schema.json. governance.virtual_keys[] is still additionalProperties: false and doesn’t define either field, so config-backed virtual keys using this new expiry support will fail schema validation.

🤖 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` at line 168, The governance
virtual key model now includes ExpiresAt, but the config schema still rejects it
because governance.virtual_keys[] is locked down with additionalProperties
false. Update transports/config.schema.json to explicitly add expires_at and
clear_expires_at to the virtual key object definition, using the same naming and
semantics as the governance types so config-backed keys validate correctly.

Source: Path instructions

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.

2 participants