Audit & approvals
Know who did what, and make dangerous actions wait for a human.
Once an agent can run your functions, two questions matter. Afterwards: who did what, and when? Beforehand: can it delete something without asking? Graft answers both:
- The audit log records every function call, whoever made it.
- Approvals make destructive functions wait until a person says yes.
This page covers both, and the one setting for jobs that run with nobody there to approve.
The audit log
Every call to a function adds a row to the audit_log table, recording:
- who called it (the actor), and which function
- the branch, the result, and how long it took
- the correlation id returned in the
x-graft-correlation-idheader - the Git commit of the code that served it
Inputs are not recorded. The log shows who ran what and how it went, not the data they sent, so it never becomes a second copy of your users' personal data. The same rows are what rate limits count.
To find one call, take the correlation id from the response and look it up. Observability shows how.
How an approval works
Mark a function destructive: true and it never runs on the first call:
agent: POST /api/fn/deleteSubmission {"id": "…"}
graft: 403 … { approvalId: "ap_…", fix: "…" } the call is held, not run
human: graft approvals list what is waiting
human: graft approve ap_… say yes
agent: repeat the call with x-graft-approval: ap_…
graft: 200 { data: … } now it runs
Over MCP, the agent passes approval: "ap_…" to run_function or
delete_content instead of the HTTP header.
Three rules make an approval hard to misuse:
| Rule | What it prevents |
|---|---|
| It works once. | Reusing one "yes" for many deletes |
| It is tied to the exact input. | Approving the deletion of record A, then using it to delete record B |
| The requester cannot approve it. | An agent approving its own request |
On a hardened deployment, the database enforces these too. Your app's database login can file an approval request and use an approved one, but it cannot decide one: only the operator login on your machine or in CI can approve or deny. A leaked app password still cannot approve its own deletes.
Asking in-band (opt-in)
The flow above sends the person to a second terminal. When you run the MCP server on your own machine, you are already sitting at it, so the server can ask you directly instead. The agent's client shows a confirmation, and saying yes completes the call in one step. MCP calls this elicitation. If the client does not support it, or the request fails, the call falls back to the approval-id flow above.
createGraftMcp({
// …
approvalElicitation: { decider: { kind: "human", id: "you@example.com" } },
});
It is meant for local stdio servers, where the person answering really is the person at the keyboard.
Choose what needs approval
approvalPolicy sets which functions wait for a person:
| Value | What waits for approval | Where it applies |
|---|---|---|
"none" (default) | Functions marked destructive: true | Everywhere |
"human" | Every function that changes data | Everywhere |
"unattended" | Nothing | HTTP functions only, never MCP |
Set it in graft.config.ts, so a change to it shows up in code review.
Jobs with nobody to approve
A nightly cleanup job or a CI migration may need to run a destructive function with no person around. Under the default policy it would wait forever. For those deployments, turn approvals off for HTTP calls:
export const approvalPolicy = "unattended";If you embed the handler yourself, pass it there instead:
createFunctionsHandler({ functions, db, approvalPolicy: "unattended" }).
Know what you are giving up. Git can bring back a deleted document, but nothing
brings back deleted operational data or a deleted
asset. "unattended" says those losses are acceptable for this deployment. The
server logs a warning on every start while it is on.
What you keep is the record. Every call still writes its audit row, and access rules and rate limits still apply.
Next steps
- Hardening makes the database enforce who can approve.
- Observability reads the audit log.
- Connecting agents covers approving from the same terminal as the agent.