Litestore · Developer docs
Platform services

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.

Search previewyour-store.com › products › linen-shirtLinen Shirt, Relaxed Fit | Your StoreA breathable linen shirt with a relaxed fit, made for warm days. Free shipping over $50.
Drafted by AI
A tool that changes something creates an approval card, and the change runs only after an operator approves 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.

ModeWhat happens when the model calls it
readrun executes inside the model loop and returns data. It changes nothing.
proposerun 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.
executerun 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:edit

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

ModeTools
readSales 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)
proposeproposeRefund, proposeCoupon, proposeCampaign, proposeInventoryAdjustment, proposeProductPriceUpdate, draftCustomerMessage
executesavePage, 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.

SettingWhat it doesDefault
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  →  expired

Adding 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?

On this page