fix(mcp): correct fiscal_periods column ref + remove agent auto-commit (#394)

* fix(mcp): correct fiscal_periods column ref + remove agent auto-commit

The MCP `gnubok_list_fiscal_periods` tool selected a non-existent
`fiscal_periods.status` column, causing `column fiscal_periods.status
does not exist` errors when agents called it. Fixed by selecting the
real columns (`is_closed`, `locked_at`, `closed_at`,
`opening_balances_set`) and deriving `status` in code.

Also removes the agent auto-commit feature (settings card, gating logic,
DB columns, tests). In its current shape only `create_customer` was
auto-commitable, so the toggle changed nothing meaningful in practice
while implying a level of agent autonomy that wasn't actually granted.
The risk-tier infrastructure on `pending_operations` (actor model,
risk_level) is kept since it's still used by the /pending UI filters.

Migration `20260505120000_drop_agent_auto_commit.sql` drops:
  - pending_operations.auto_commit_eligible
  - pending_operations.auto_committed_at
  - company_settings.agent_auto_commit_enabled
  - company_settings.agent_auto_commit_max_amount

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): explicitly pick fields in gnubok_list_fiscal_periods response

Greptile flagged that `...p` spreads raw DB columns (`is_closed`,
`locked_at`, `closed_at`) into the tool response alongside the
derived `status`. Drop the spread for an explicit field list so the
tool contract is the derived status only — agents don't need to
reason about raw columns, and future SELECT additions won't silently
leak.

Also drops `closed_at` from the SELECT since it wasn't read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-05-05 21:16:01 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 5c52f24a49
commit b05f0e59b9
13 changed files with 62 additions and 686 deletions
+2 -91
View File
@@ -19,7 +19,6 @@ import {
CheckCircle2,
XCircle,
Bot,
Sparkles,
} from 'lucide-react'
import type { PendingOperation, PendingOperationStatus } from '@/types'
@@ -168,9 +167,7 @@ function OperationPreview({ op }: { op: PendingOperation }) {
}
}
type SourceFilter = 'all' | 'agent' | 'auto_committed' | 'high_risk'
const AUTO_COMMIT_BANNER_DISMISS_KEY = 'gnubok.pending.autoCommitBannerDismissedAt'
type SourceFilter = 'all' | 'agent' | 'high_risk'
export default function PendingOperationsPage() {
const [operations, setOperations] = useState<PendingOperation[]>([])
@@ -181,8 +178,6 @@ export default function PendingOperationsPage() {
const [selectedOp, setSelectedOp] = useState<PendingOperation | null>(null)
const [showCommitDialog, setShowCommitDialog] = useState(false)
const [isCommitting, setIsCommitting] = useState(false)
const [recentAutoCommits, setRecentAutoCommits] = useState<PendingOperation[]>([])
const [bannerDismissedAt, setBannerDismissedAt] = useState<number | null>(null)
const { toast } = useToast()
const { dialogProps, confirm } = useDestructiveConfirm()
@@ -202,38 +197,6 @@ export default function PendingOperationsPage() {
fetchOperations()
}, [fetchOperations])
useEffect(() => {
const stored = typeof window !== 'undefined'
? window.localStorage.getItem(AUTO_COMMIT_BANNER_DISMISS_KEY)
: null
setBannerDismissedAt(stored ? Number(stored) : null)
fetch('/api/pending-operations?status=committed&limit=50')
.then((r) => r.json())
.then((json) => {
const ops: PendingOperation[] = json.data ?? []
const cutoff = Date.now() - 24 * 60 * 60 * 1000
setRecentAutoCommits(
ops.filter((o) => o.auto_committed_at && new Date(o.auto_committed_at).getTime() > cutoff)
)
})
.catch(() => { /* silent */ })
}, [])
function dismissAutoCommitBanner() {
const now = Date.now()
setBannerDismissedAt(now)
if (typeof window !== 'undefined') {
window.localStorage.setItem(AUTO_COMMIT_BANNER_DISMISS_KEY, String(now))
}
}
const newAutoCommits = recentAutoCommits.filter((o) =>
bannerDismissedAt == null ||
(o.auto_committed_at != null &&
new Date(o.auto_committed_at).getTime() > bannerDismissedAt)
)
async function handleCommit() {
if (!selectedOp) return
setIsCommitting(true)
@@ -284,8 +247,6 @@ export default function PendingOperationsPage() {
switch (sourceFilter) {
case 'agent':
return op.actor_type === 'api_key' || op.actor_type === 'mcp_oauth' || op.actor_type === 'cron'
case 'auto_committed':
return Boolean(op.auto_committed_at)
case 'high_risk':
return op.risk_level === 'high'
case 'all':
@@ -301,49 +262,6 @@ export default function PendingOperationsPage() {
description="Operationer som väntar på godkännande"
/>
{newAutoCommits.length > 0 && (
<Card className="border-sage/40 bg-sage/5">
<CardContent className="flex items-start justify-between gap-4 py-3">
<div className="flex items-start gap-3">
<Sparkles className="h-4 w-4 mt-0.5 text-sage-foreground" />
<div className="text-sm">
<p className="font-medium">
{newAutoCommits.length === 1
? '1 åtgärd auto-godkändes'
: `${newAutoCommits.length} åtgärder auto-godkändes`}{' '}
senaste dygnet
</p>
<p className="text-xs text-muted-foreground mt-0.5">
Granska vad agenten utförde utan din direkta godkännande.
</p>
</div>
</div>
<div className="flex gap-2 flex-shrink-0">
<Button
size="sm"
variant="outline"
className="h-8 px-3 text-xs"
onClick={() => {
setActiveTab('committed')
setSourceFilter('auto_committed')
dismissAutoCommitBanner()
}}
>
Visa
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 px-3 text-xs"
onClick={dismissAutoCommitBanner}
>
Dölj
</Button>
</div>
</CardContent>
</Card>
)}
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as PendingOperationStatus)}>
<TabsList>
<TabsTrigger value="pending">Väntande</TabsTrigger>
@@ -356,7 +274,6 @@ export default function PendingOperationsPage() {
<TabsList>
<TabsTrigger value="all">Alla</TabsTrigger>
<TabsTrigger value="agent">Från agent</TabsTrigger>
<TabsTrigger value="auto_committed">Auto-godkända</TabsTrigger>
<TabsTrigger value="high_risk">Hög risk</TabsTrigger>
</TabsList>
</Tabs>
@@ -417,13 +334,7 @@ export default function PendingOperationsPage() {
{op.actor_label || op.actor_type}
</Badge>
)}
{op.auto_committed_at && (
<Badge variant="outline" className="bg-sage/10 border-sage/40 text-sage-foreground">
<Sparkles className="h-3 w-3 mr-1" />
Auto-godkänd
</Badge>
)}
{op.status === 'committed' && !op.auto_committed_at && (
{op.status === 'committed' && (
<Badge variant="default" className="bg-emerald-500/10 text-emerald-600 border-emerald-200">
<CheckCircle2 className="h-3 w-3 mr-1" />
Godkänd
-2
View File
@@ -1,12 +1,10 @@
'use client'
import { ApiKeysPanel } from '@/components/settings/ApiKeysPanel'
import { AgentAutoCommitSettings } from '@/components/settings/AgentAutoCommitSettings'
export default function ApiSettingsPage() {
return (
<div className="space-y-8">
<AgentAutoCommitSettings />
<ApiKeysPanel />
</div>
)