Skip to main content

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:

NeedTool
A person decides before an agent actsTool approval and human gates
A record of each decision that can't be edited unnoticedSigned decisions
Proof that a run happened as recordedReplay
No secrets or personal data sent to a model or written to logsRedaction 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.
  • InMemoryApprovalEngine is for development. In production, implement the ApprovalEngine interface 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.

caution

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​