Governance
Governance in Ailu answers three questions about every run: who allowed each sensitive action, what the agent saw and did, and can we prove it later. The building blocks:
| Need | Tool |
|---|---|
| A person decides before an agent acts | Tool approval and human gates |
| A record of each decision that can't be edited unnoticed | Signed decisions |
| Proof that a run happened as recorded | Replay |
| No secrets or personal data sent to a model or written to logs | Redaction and noLog |
Sign approval decisions
An approval engine keeps the approval requests and who resolved them. An attestor signs each decision with Ed25519 and links it to the previous one, so removing or editing a record breaks the chain.
import { Ed25519Attestor, InMemoryApprovalEngine, verifyChain, type NodeId, type RunId } from "@ailu-ai/graph-sdk";
const approvals = new InMemoryApprovalEngine();
// Keep the private key in your secret store; publish the public key to your auditors.
const attestor = new Ed25519Attestor();
// An agent asked to refund; a named human approved it.
const request = await approvals.request({
runId: "run-42" as RunId,
nodeId: "assistant" as NodeId,
requestedBy: "assistant",
subject: { description: "refund order ORD-8830" }
});
const approved = await approvals.approve(request.id, "alice@example.com");
// Sign each decision, chained to the previous one.
const first = attestor.attest(approved, null);
const records = [first];
// Anyone holding the records can check them. Also check that record.publicKey is your key.
console.log(verifyChain(records)); // true
approvals.approve(id, user)refuses a user who is also the requester (ApprovalSelfApprovalError), and refuses to resolve a request twice.verifyChain(records)checks the links and every signature. It checks each signature against the public key stored in the record, so also compare that key with the one you published.InMemoryApprovalEngineis for development. In production, implement theApprovalEngineinterface over your database.
To file approval requests automatically when a run stops, run the graph with the catalog runner
and pass the engine: runCatalogGraph(app.definition, { approvalEngine, tools }). Each gated
tool call and each human gate gets a request you can list with approvalEngine.getPending(runId);
their ids are also saved in the run's state.
Pass the same engine to resumeCatalogGraph. It checks the engine before anything runs, and
throws ApprovalNotGrantedError if a request the run waits on is still pending, a human gate was
rejected, or a tool in approvedTools isn't approved by the person the grant names.
Resume across processes shows it.
Without approvalEngine, resumeCatalogGraph doesn't check anything: it continues past a human
gate.
Audit exports from Ailu Studio can be checked by anyone, offline:
npx @ailu-ai/verify capsule.json --key <published public key>.
Replay a run
With AILU_LLM_RECORD=1, the catalog runner records every model call and timestamp of the run.
Store the recording with the run. Later, replay it: the engine re-runs the graph from its first
state, feeding it the recorded model outputs instead of calling a model, and must reach the same
result.
import { docQaReferenceDefinition, replayCatalogGraph, runCatalogGraph } from "@ailu-ai/graph-sdk";
const definition = docQaReferenceDefinition();
// 1. Record: with AILU_LLM_RECORD=1 the engine journals every model call and timestamp.
process.env.AILU_LLM_RECORD = "1";
const recorded = await runCatalogGraph(definition, {
initialData: { question: "How does Ailu resume a run?", documents: "Ailu checkpoints after every node." }
});
delete process.env.AILU_LLM_RECORD;
// Store these two with the run: they are the evidence.
const { entryState, replayJournal } = recorded;
// 2. Replay, later and elsewhere: the run is re-derived from its entry state
// using the recorded model outputs. No model is called.
const replayed = await replayCatalogGraph(definition, entryState!, "audit-1", replayJournal!);
console.log(JSON.stringify(replayed.state.channels.answer) === JSON.stringify(recorded.state.channels.answer)); // true
To compare the approval decisions of a replay with the signed chain, use
verifyReplayDecisions(attested, replayed): it returns ok and the list of mismatches.
Redact secrets and personal data
Before any text reaches a model, the engine scans it for secrets (API keys, tokens, private keys)
and masks them. This is always on. Set AILU_SECRETS_POLICY=block to fail the call instead of
masking.
For personal data (names, emails, account numbers), point the engine at a redaction service with
AILU_PII_REDACTOR_URL. The service receives the outgoing texts and returns them redacted. If it
is unreachable, the text is sent unredacted unless you set AILU_PII_REDACTOR_FAIL_CLOSED=1,
which is what you want in production.
Keep secrets out of logs
A channel marked noLog: true is masked in every run event, so it never reaches your logs or
traces. It is still checkpointed, so the run can resume.
import { createGraph, type RunEvent } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "kyc" })
// noLog: the value is checkpointed as usual but masked in every run event and log.
.channel("passport", { type: "string", default: "", noLog: true })
.channel("verified", { type: "boolean", default: false })
.node("check", async (_input, state) => ({ verified: state.channels.passport.length > 0 }))
.compile();
const events: RunEvent[] = [];
app.onEvent((event) => events.push(event));
const out = await app.run({ passport: "X1234567" });
console.log(out.channels.verified); // true
console.log(JSON.stringify(events).includes("X1234567")); // false
Next
- Send traces and costs to your observability stack: Observability.
- Production settings: Deploy to production.