Manual setup
Every line Veefore needs in your codebase, written by hand — what to add, where it goes, and how to tell which shape your code is.
veefore integrate writes these changes for you. This page is for teams that
will not let a tool edit their source: it lists every line Veefore needs, where
each one goes, and how to decide which shape applies to your code.
Nothing here is a shortcut. If you add what this page describes, the result is identical to what the automatic rewrite produces.
#The three ways to get the code in
| Route | What happens | When to use it |
|---|---|---|
| Veefore AI | veefore integrate sends the region of a file that needs changing to a model Veefore hosts, shows you the diff, writes nothing until you accept |
You want it done and are comfortable with code leaving your machine to Veefore and its model provider |
| Your own API key | The same, except the request goes directly from your machine to Anthropic or OpenAI with your key. Code never reaches Veefore | You want AI help but your code may only go to a provider you already have an agreement with |
| By hand | This page. The CLI prints the same instructions with your real file and line numbers | Review requirements, air-gapped work, or you simply want to know what is being added |
Either AI route shows a diff first, keeps a backup, and rolls back if your own checks newly fail. Pick the route at the prompt, or force it:
bashveefore integrate --assist=manual # print instructions, change nothing
veefore integrate --assist=own # your Anthropic or OpenAI key
veefore integrate --assist=veefore # Veefore-hosted model
veefore integrate --assist=off # skip the offer entirely
veefore integrate --check # verify every tool is protected, change nothing
--assist=manual is worth running even if you intend to type everything
yourself: it names your files and lines rather than leaving you to map this
page onto your project.
#What you are adding, in one sentence each
| Line | What it does |
|---|---|
guard('tool_key', fn) |
Wraps a tool's function. Veefore decides before the function runs |
await checkpoint('tool_key', args) |
A gate with nothing to wrap. Veefore decides before the next statement runs |
const notice = approvalNotice(error) |
Turns an approval that was raised into a sentence for the user |
await resumeApprovals() |
Finishes actions a human approved since the last turn |
{ onApproval: 'defer' } |
Return immediately instead of blocking when a human must approve |
#Step 1 — install and configure
bashnpm install @veefore/sdk # or: pip install veefore-py
veefore init writes .env.local. By hand, it needs:
bashVEEFORE_GATEWAY_URL=https://gateway.veefore.com
VEEFORE_AGENT_ID=agt_…
VEEFORE_AGENT_KEY_ID=key_…
VEEFORE_AGENT_PRIVATE_KEY=… # generated locally; Veefore never receives it
VEEFORE_APPROVAL_MODE=defer # wait | defer | throw — see step 3
The private key is generated on your machine and written to that file only. Veefore cannot recover it. Move it into your secret manager before deploying.
#Step 2 — protect each tool
Which line you add depends on the shape of your code. Work through these in order; the first one that matches is the one to use.
#Shape A — the tool is a function you can wrap
ts// before
export const refundOrder = async ({ orderId }) => {
return await payments.refund(orderId);
};
// after
import { guard } from '@veefore/sdk';
export const refundOrder = guard('refund_order', async ({ orderId }) => {
return await payments.refund(orderId);
});
guard() returns a promise, so the wrapped function must be async. If it
was not, make it so.
A method becomes a property:
ts// before: async execute(args) { … }
// after:
execute: guard('refund_order', async (args) => { … }),
Python uses a decorator, directly above the def and below any framework
decorator:
pythonfrom veefore import guard
@tool
@guard("refund_order")
async def refund_order(order_id: str) -> dict:
return await payments.refund(order_id)
#Shape B — tools are data, and one place runs whichever the model picked
Add exactly one guard, around the call that performs the chosen tool, keyed by the expression holding its name. Not one guard per tool.
ts// before
const result = await executor.execute(call);
// after
const result = await guard(call.tool, (_args: typeof call.params) =>
executor.execute(call),
)(call.params);
call.params matters: that is what your policies read. Pass the property
holding the tool's own arguments, never the whole call object when such a
property exists.
#Shape C — the tool runs inside a branch, with no function to wrap
This is the common shape in an agent loop, and the one most people get wrong.
There is nothing to wrap, so use checkpoint() as the first statement of
the block that performs the action:
tsimport { checkpoint } from '@veefore/sdk';
if (name === 'delete_post') {
await checkpoint('delete_post', args);
await storage.deleteContent(contentId);
return { ok: true, message: 'Post deleted.' };
}
Put it where the action happens, not where it is described
Agents often branch on the same tool key twice — once to build a confirmation card for the user, once to carry the action out:
tsif (name === 'delete_post') { // builds a card. NOT here.
return { card: { action: 'delete_post' } };
}
…
if (name === 'delete_post') { // deletes. HERE.
await checkpoint('delete_post', args);
await storage.deleteContent(contentId);
}
A gate on the card can never cause a deletion however many times it is
approved, because checkpoint() completes by being reached again. Worse, the
real deletion then runs unchecked. Find the branch that performs the side
effect — the one that awaits a write — and gate that one.
#Shape D — the tool is on an MCP server or another agent
Nothing to wrap. The bridge call is itself the protected action:
tsconst outcome = await client.mcpCall({
server: 'srv_files',
tool: 'delete_file',
arguments: { path },
});
Approvals work the same as for guard(); see step 3.
#Step 3 — choose what happens when a human must approve
One setting, three behaviours. Set it once in VEEFORE_APPROVAL_MODE, or per
call.
| Mode | Behaviour | Use for |
|---|---|---|
wait |
Block until the decision lands, then run the action | Batch jobs, queue workers, anything with no user waiting |
defer |
Return immediately with a sentence for the user; finish on a later turn | Chat agents. A turn times out in seconds; a reviewer does not |
throw |
Raise and do not perform the action | You want approvals to be a hard failure |
wait is the default. If your agent talks to a person, you want defer:
bashVEEFORE_APPROVAL_MODE=defer
Per call, if you need to differ from the project setting:
tsguard('refund_order', refundOrder, { onApproval: 'defer' })
await checkpoint('delete_post', args, { onApproval: 'defer' })
await client.mcpCall({ … }, { onApproval: 'defer' })
Python: on_approval="defer".
Deferring is two halves
Returning immediately is half of it. Collecting the decision later is the
other, and the SDK cannot add it for you — it is in your turn loop, which is
framework-specific and usually nowhere near a tool. A project with the first
half and not the second is in the worst state Veefore can put it in: the
reviewer approves, the agent promised the user it would happen, and nothing
does. veefore doctor fails until both halves are present.
#Step 4 — the two lines in your turn loop
These are the lines no automatic rewrite will add for you.
#4a — tell the user, instead of reporting a failure
A checkpoint() raises when a human must approve. Agent code normally
catches everything and reports it as "something went wrong", so without this
branch the user is told the action failed while it sits in the approval queue,
working perfectly.
Add these as the first statements of the catch where an error becomes the
assistant's reply:
tsimport { approvalNotice } from '@veefore/sdk';
try {
return await runAgentTurn(message);
} catch (error) {
const notice = approvalNotice(error);
if (notice) return notice.say;
throw error; // real errors still behave as before
}
Three details that decide whether this works:
- Use the name the block already binds.
catch (err)meansapprovalNotice(err). If it binds nothing, change it tocatch (error). - Reply the way the block already replies. If its other returns are
objects, match them:
return { summaryText: notice.say }, not a barenotice.say. A bare string where the caller expects an object is a type error that often will not show up until runtime. - Every catch that can receive one. A file can have several protected
calls landing in different
catchblocks thousands of lines apart. A branch in one of them leaves the others reporting failures.veefore doctorlists every uncovered block with its file and line.
Python:
pythonfrom veefore import approval_notice
try:
return await run_agent_turn(message)
except Exception as error:
notice = approval_notice(error)
if notice:
return notice["say"]
raise
Where that catch lives depends on your framework:
| Your code | Where the branch goes |
|---|---|
try/catch around the turn |
That catch |
Express, errors in app.use |
app.use((err, req, res, next) => …) — the four-argument one |
| Promise chain | The .catch() whose value becomes the reply |
Python try/except |
That except |
#4b — collect the decision on a later turn
tsimport { resumeApprovals, withApprovalSession } from '@veefore/sdk';
async function onMessage(chatId: string, text: string) {
// One session per conversation, so one user never finishes another
// user's approved action.
return withApprovalSession(chatId, async () => {
// First, before the model runs: finish anything decided since last turn.
for (const done of await resumeApprovals()) {
history.push({ role: 'system', content: done.message });
}
return runModel([...history, { role: 'user', content: text }]);
});
}
Python: resume_approvals(), or resume_approvals_async(), inside
with_approval_session(chat_id).
What each protection does when the decision arrives:
| Protection | How it finishes |
|---|---|
guard() |
The SDK holds your function and runs it. Done |
| Bridge call | Veefore replays the approved request and makes the call. Done. Nothing local needs registering |
checkpoint() |
Completes when the agent reaches the same checkpoint again. resumeApprovals() reports the decision but cannot run the block |
That last row is why a checkpoint-protected action needs the user to ask again,
or your code to re-enter that path, after approval. resumeApprovals() returns
approved_retry for those so you can tell the user it is cleared to go.
#Step 5 — verify
bashveefore integrate --check # every registered tool reaches Veefore
veefore doctor # the whole setup, including both defer halves
veefore dev # watch decisions arrive live
veefore doctor fails, rather than warns, on the mistakes that are invisible
at runtime:
deferis set but nothing callsresumeApprovals()- a
checkpoint()exists but nocatchturns its raise into a reply - a
catchthat receives an approval has no branch, even though another one does - the installed SDK is too old to honour
onApproval: 'defer'— it would ignore the option and wait, which is the hang you setdeferto avoid
#How a chat agent behaves once this is in
Worth reading before you ship, because the sequence surprises people:
- The user asks for something that needs approval.
- The tool reaches Veefore. Policy says a human must decide.
- With
defer, the call returns at once. Yourcatchbranch (4a) replies withnotice.say— something like "that needs a sign-off first". - A reviewer approves in the console.
- On the next turn,
resumeApprovals()(4b) finishes it and hands you a message to add to the history, so the agent can tell the user it is done.
For a guard()-protected tool or a bridge call, step 5 performs the action.
For a checkpoint(), step 5 reports the decision and the action runs when that
code path is reached again.
#Minimum versions
| Feature | SDK |
|---|---|
guard(), approvals, waiting |
1.0.0 |
checkpoint() |
1.3.0 |
onApproval: 'defer', resumeApprovals(), approvalNotice() |
1.6.0 |
onApproval on mcpCall / a2aInvoke |
1.7.0 |
An older SDK does not reject a newer option — it ignores it. A project setting
defer against SDK 1.5 waits instead, and the turn hangs. veefore doctor
reports the mismatch.