Audit & approvals

Know who did what, and make dangerous actions wait for a human.

Runtime data4 min read

needs Postgres

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-id header
  • 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:

RuleWhat 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:

ValueWhat waits for approvalWhere it applies
"none" (default)Functions marked destructive: trueEverywhere
"human"Every function that changes dataEverywhere
"unattended"NothingHTTP 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:

graft.config.ts
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