Agents
Add tools to the admin's AI assistant that read data freely and only change data after an operator approves the change.
The admin's AI assistant uses a tool kit, and Litestore sorts each tool by what it is allowed to do. Read this page before you add a tool, so you pick the right mode for it.
Read tools run inside the model loop and answer straight away. Tools that would change something never run when the model calls them. Instead, they record a proposal as an AgentRun row on the server, and the change only happens when an operator approves that row in the admin.
We built it this way because Litestore checks permissions at the server-action boundary, while agent tools call the services and CRUD layer underneath it. A raw tool that called CRUD directly would skip every permission check. The tool kit puts the check back at the tool layer. Any agent therefore gets the least privilege it needs, has to ask for confirmation and can only propose money changes, without extra work.
Modes
Every tool declares a permission, a mode, a parameter schema and a run function. The mode determines what happens when the model calls the tool.
| Mode | What happens when the model calls it |
|---|---|
read | run executes inside the model loop and returns data. It changes nothing. |
propose | run doesn't execute. The call writes an AgentRun proposal row (tool and parameters stored on the server) and returns a pending action with the row's proposalId. |
execute | run doesn't execute either. At call time it behaves exactly like propose, and it only fires from approveAgentProposal after an operator approves. |
The model can't trigger propose or execute tools by itself, so that isn't what separates them. The difference is the kind of action the tool may perform. You use execute for content changes. A protected domain may only use propose.
The operator's click is the authorization. The model can't satisfy a confirmation step on its own, and it can't approve a row. There is no confirm flag either, because a boolean the model controls could never prove that a person approved.
The model's own instructions tell it this. A tool that changes something returns needs_approval, and the model must never claim the action is done based on that result.
callAgentTool in modules/assistant/agent-tool-kit.ts is the single gate, and every tool is defined in modules/assistant/agent-tools.ts. The chat loop in the admin and the operator MCP endpoint both call the gate, so exactly one piece of code determines who may use a tool and what happens when it changes something.
If the acting user lacks a tool's permission, the model still receives the tool, but calling it returns forbidden. We did this on purpose. The assistant can then explain why it can't act, instead of behaving as if the tool doesn't exist.
Proposed actions
Money, refunds and stock never get an execute tool. Code review doesn't need to check for this rule. If a tool declares execute on a permission in the forbidden set, Litestore throws an error while building the tool map, and the failure appears at startup instead of in production.
The forbidden set is:
processFinancials
dangerZone
refunds:edit
refunds:delete
collabs:editLitestore runs this check on every tool in the registry, including tools above the current autonomy ceiling. A hidden tool with an illegal declaration is still a developer mistake, and hiding it until someone raises the ceiling would only postpone the error.
An admin:mcp key reaches this surface
The operator MCP endpoint gives an outside assistant the same tool kit. Its change tools still only create approval cards, so the assistant can't move money. You should still treat the key like an admin credential. See API Keys.
Tools
The registry holds 43 tools, and most of them read.
| Mode | Tools |
|---|---|
read | Sales and reports (getStoreSnapshot, getRevenue, getOrderSummary, getRecentOrders, getTopProducts, getSalesBySource, getConversionFunnel, getCohorts, getGrowth, getProductMargins, getBusinessCostOverview, checkRevenueReconciliation), customers and marketing (getCustomerInfo, getCustomerSummary, getLoyalty, getCampaignPerformance, getLinkAttribution, getAbandonedCarts, getCartRecovery, countBroadcastAudience), catalog and stock (searchProducts, getProduct, getLowStockCount, getReorderForecast, findMedia, findShippingRates, getChannels, getStorefront), history (searchActivity) and outside data (scrapeCompetitorData, extractProductData, extractCompetitorPrice) |
propose | proposeRefund, proposeCoupon, proposeCampaign, proposeInventoryAdjustment, proposeProductPriceUpdate, draftCustomerMessage |
execute | savePage, updateFeed, setSeoMeta, updateProductContent, saveTranslation, sendBroadcastEmail |
Approval flow
A proposal is a row, not a token. recordProposal in modules/assistant/agent-runs.ts stores the tool name, the parameters and the proposing user, and the approval card renders from that row by its id. Because the server stores the parameters, the operator approves exactly what the card shows: nobody can redeem the proposal for a different change.
approveAgentProposal in modules/assistant/admin/approval-actions.ts is the only code that runs a change-making tool. It checks everything again before anything happens.
Proposal lookup
It loads the row for the operator who is approving it. A proposal belongs to one user.
Availability and permission
assertToolRunnable confirms the tool still exists, that the autonomy ceiling still allows it (the operator may have lowered it after the proposal was made) and that the operator still holds the tool's own permission.
Parameter parsing
It parses the stored parameters against the tool's schema again.
Claim
claimProposal runs a compare-and-set UPDATE that moves the row from proposed to executed, and only while the proposal is less than five minutes old.
Tool execution
Only then does run execute, and an ai_action_executed activity entry records who approved it.
When several requests try to approve the same proposal at once, whether through a replay, a double-click or a race with dismiss, exactly one of them receives a row count of 1. Litestore rejects the rest with "This proposal expired or was already handled".
Claiming the row before the side effect closes the gap between checking and acting. Once Litestore claims the proposal, it is spent, even if run throws.
Dismissing a proposal uses the same mechanism in the other direction, with a compare-and-set from proposed to dismissed.
Autonomy ceiling
Operators control one setting, so they can open and close capabilities at runtime without a deploy.
| Setting | What it does | Default |
|---|---|---|
ai_max_autonomy (Settings → AI) | Sets the highest mode the assistant may use: read, propose or execute. | read |
Raising the ceiling lets the model propose more, within the limits above. It can't turn a propose-only tool into one that executes, because Litestore enforces that boundary where it builds the tools, not through this setting.
If a tool's mode is above the ceiling, Litestore leaves it out of the tool map entirely. The model never receives a tool it can't use.
Server actions that create proposals outside the chat loop check the same ceiling before they generate anything, so a capability above it never spends tokens producing an approval card that would fail when approved.
Agent run ledger
AgentRun records proposals that were approved, dismissed and expired. An expired proposal shows that the assistant suggested something nobody acted on, which is exactly the record you need when you audit what an assistant has been doing.
Litestore works out expiry instead of writing it down, and no cron job sweeps the table. deriveProposalState reads a proposed row older than PROPOSAL_TTL_MS (five minutes) as expired, and claimProposal refuses the same rows, so the displayed state and the enforcement use one constant. The weekly data cleanup deletes settled rows after 180 days.
proposed → executed
proposed → dismissed
proposed → expiredAdding tools
Declare the tool as read when it only returns data. Read is the only mode that runs in the loop and the only one that needs no operator.
When you design a new tool, don't ask whether the model can call it safely. Ask this instead: if this runs when nobody meant it to, what can't be undone, and is that domain propose-only?
Related
AI
Litestore calls Google Gemini, Anthropic Claude or OpenAI, falls back along a failover chain, and ships a specific set of AI features.
MCP
AI assistants connect to the store over MCP. Shoppers get the public tools at /api/mcp, and the admin's gated tool kit answers at /api/mcp/admin behind an admin:mcp key.
Permissions
Litestore turns an admin role into effective permissions, grantedPermissions and deniedPermissions override the role, and a permission procedure wraps each server action or page.