bb855d2ddc
* 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
140 lines
4.3 KiB
TypeScript
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,
|
|
}
|
|
}
|