Files
accounted/lib/pending-operations/should-auto-commit.ts
T
Mattsson bb855d2ddc Add/ai native supp (#385)
* feat(branding): implement dynamic branding in service worker and reports

* feat(auth): enhance API key scopes and add bookkeeping write scope

- Updated transaction write scope description to include additional tools.
- Enhanced reports read scope description to reflect new functionality.
- Introduced bookkeeping write scope with relevant description.
- Updated SCOPE_GROUPS to include bookkeeping domain.
- Modified TOOL_SCOPE_MAP to include new bookkeeping operations.
- Updated validateApiKey function to return api_key_id and api_key_name for better actor attribution.

feat(tests): add unit tests for MCP resource registry

- Created tests for data resources to ensure all required fields are present.
- Added tests for resource query parsing and retrieval.

feat(resources): implement MCP resources for company and accounting data

- Added capabilities resource to expose API key capabilities based on granted scopes.
- Implemented chart of accounts resource to retrieve active BAS chart.
- Created company current resource to fetch active company details.
- Developed active fiscal period resource to check posting eligibility.
- Implemented recent activity resource to fetch latest journal entries, invoices, and transactions.
- Added VAT treatments resource to provide available VAT rates per customer type.

feat(pending-operations): introduce risk tiers for operations

- Added risk level classification for pending operations to determine auto-commit eligibility.
- Implemented functions to classify operation risk levels and identify high-risk operations.

feat(migrations): add actor model and risk tier to pending operations

- Updated pending_operations table to include actor type and risk level columns.
- Enhanced audit_log to mirror actor information for compliance.
- Modified validate_and_increment_api_key function to return actor details.
- Expanded operation types in pending_operations to include new high-risk operations.

* feat: add auto-commit functionality for low-risk pending operations

- Implemented shouldAutoCommit function to determine eligibility for auto-commit based on operation type, actor type, and company settings.
- Created commitPendingOperation function to handle execution of pending operations with consistent status updates.
- Added tests for shouldAutoCommit to cover various scenarios including high-risk operations, user actors, company opt-in status, and monetary thresholds.
- Introduced new columns in company_settings for agent_auto_commit_enabled and agent_auto_commit_max_amount to allow companies to opt-in for auto-commit functionality.
- Added SQL migration to update the database schema for new auto-commit settings.

* feat(idempotency): implement idempotency key handling for safe retries and cleanup

* feat: expand API key scopes and pending operations for bookkeeping

- Added 'suppliers:write' scope to API key scopes for supplier invoice management.
- Updated SCOPE_GROUPS to include the new 'suppliers:write' scope.
- Introduced new pending operation types for bookkeeping: close_period, lock_period, run_year_end, set_opening_balances, run_currency_revaluation, explain_voucher_gap, uncategorize_transaction, approve_supplier_invoice, credit_supplier_invoice, and convert_invoice.
- Implemented corresponding commit functions for the new operations in the pending operations module.
- Enhanced PendingOperation type to include actor model and risk level attributes.
- Added tests for new functionality, ensuring proper behavior and constraints in the database.

* feat: implement unlockPeriod functionality and related tests

* feat: add agent auto-commit settings and related functionality

* feat: add attention resource with comprehensive summary of outstanding tasks

* feat: enhance pending operations with 'committing' status and immutability checks, improve idempotency handling, and add original voucher reference for credit notes
2026-05-04 11:12:29 +02:00

140 lines
4.3 KiB
TypeScript

/**
* Decide whether a freshly-staged pending_operation should be auto-committed
* by a trusted agent without human approval.
*
* Defense-in-depth: high-risk operations (period close, year-end, send_invoice,
* etc.) are NEVER auto-committed regardless of company settings or actor
* trust — that gate lives in risk-tiers.ts and is checked here before any
* config lookup.
*
* Trust hierarchy:
* - 'user' actors are humans clicking in the UI; auto-commit doesn't apply
* (the click IS the approval)
* - 'api_key' / 'mcp_oauth' actors are agents; eligible for auto-commit if
* the company opts in and the op is low-risk
* - 'cron' actors are system tasks; always auto-commit (they have no
* human in the loop by design)
*
* Monetary threshold:
* When `agent_auto_commit_max_amount` is set, any low-risk op with a
* preview/payload amount above the threshold falls back to human approval.
* The amount is read from the preview_data — callers should put it under
* `amount` or `total` for the gate to find it. Missing amount → not blocked
* by the threshold (safe for ops like create_customer where there's no
* single dollar value).
*/
import type { SupabaseClient } from '@supabase/supabase-js'
import { isHighRisk, getRiskLevel, type RiskLevel } from './risk-tiers'
export type AutoCommitActorType = 'user' | 'api_key' | 'mcp_oauth' | 'cron'
export interface AutoCommitInput {
operationType: string
actorType: AutoCommitActorType
/** Optional monetary amount to check against agent_auto_commit_max_amount. */
amount?: number | null
}
export interface AutoCommitDecision {
eligible: boolean
reason: string
risk_level: RiskLevel
}
/**
* Cheap pure-logic check that doesn't hit the DB. Used to short-circuit
* obvious "no" cases before reading company_settings.
*/
function precheck(input: AutoCommitInput): AutoCommitDecision | null {
const risk = getRiskLevel(input.operationType)
if (isHighRisk(input.operationType)) {
return {
eligible: false,
reason: `Operation "${input.operationType}" is high-risk and never auto-committed.`,
risk_level: risk,
}
}
if (input.actorType === 'user') {
return {
eligible: false,
reason: 'User actors approve via the UI; auto-commit does not apply.',
risk_level: risk,
}
}
// Cron actors auto-commit non-high-risk regardless of company config.
// Resolved here so we don't read company_settings unnecessarily.
if (input.actorType === 'cron') {
return {
eligible: true,
reason: 'Cron actor: auto-commit allowed for non-high-risk ops.',
risk_level: risk,
}
}
// For api_key/mcp_oauth: only low-risk is auto-committable in this phase.
// Reject medium-risk before the company_settings lookup so callers don't pay
// for a DB read that can't succeed.
if (risk !== 'low') {
return {
eligible: false,
reason: 'Only low-risk operations are auto-committable in the current phase.',
risk_level: risk,
}
}
return null
}
export async function shouldAutoCommit(
supabase: SupabaseClient,
companyId: string,
input: AutoCommitInput
): Promise<AutoCommitDecision> {
const pre = precheck(input)
if (pre) return pre
const risk = getRiskLevel(input.operationType)
// api_key / mcp_oauth + low-risk: gated by company opt-in and threshold.
const { data: settings, error } = await supabase
.from('company_settings')
.select('agent_auto_commit_enabled, agent_auto_commit_max_amount')
.eq('company_id', companyId)
.maybeSingle()
if (error || !settings) {
return {
eligible: false,
reason: 'Could not read company settings; defaulting to human approval.',
risk_level: risk,
}
}
if (!settings.agent_auto_commit_enabled) {
return {
eligible: false,
reason: 'Company has not opted in to agent auto-commit.',
risk_level: risk,
}
}
const max = settings.agent_auto_commit_max_amount
const amount = input.amount
if (max != null && amount != null && Math.abs(amount) > Number(max)) {
return {
eligible: false,
reason: `Amount ${amount} exceeds company auto-commit threshold ${max}.`,
risk_level: risk,
}
}
return {
eligible: true,
reason: 'Low-risk op, trusted actor, company opted in, amount within limit.',
risk_level: risk,
}
}