Lifecycle and audit
Every tool call is run by one engine, whichever surface made it, and that engine records what it did at each step. The record is the lifecycle. What happened, who did it and under what scope also goes to the audit record, which is hash-chained so that tampering with it is detectable.
One identifier ties the two together and ties both back to the caller.
lifecycle_id is the thread
The gateway mints a lifecycle_id the moment a call arrives and before anything is decided about it.
That identifier then appears in every place the call leaves a mark:
- in the successful result, which every tool returns as
{ "lifecycle_id": ..., "result": ... } - in the error body of a refused call, and in
lifecycleId/lifecycle_idon the SDK error object - in the pending payload when the call parks at an approval
- on the policy decision written for the call
- on every audit event the call produces
- on the lifecycle record itself, and on its trace
Keep it. It is the one value that turns "an agent did something odd on Tuesday" into a specific retrievable record.
The ten stages
A governed call passes through ten stages in a fixed order. Each is recorded with a status — ok,
failed, pending or skipped — and a short detail line.
| Stage | What it does |
|---|---|
S1:authenticate | The credential was verified by the transport. Recorded here so the trail is complete. |
S2:resolve-scope | The organization, department, project and role are pinned, and the tool's required grant is checked. |
S3:rate-quota | The quota buckets for the project and the organization are consumed. |
S4:validate-input | The tool checks its own arguments and reports which graph labels the call touches. |
S5:policy | The policy engine returns allow, deny or allow+gate, and the decision is recorded. |
S6:approval | A gated call parks here; a resumed call satisfies the gate here; an ungated call skips it. |
S7:dry-run | The tool simulates the call and returns a human-readable summary of what it would do. |
S8:execute | The work happens. |
S9:post-process | The result is shaped and redacted before it leaves. |
S10:record | The audit event closing the call is appended. |
The order is what makes the guarantees legible. Scope is pinned before quota is consumed; quota is consumed before the tool has looked at the arguments; policy runs after validation, so it can see the labels; execution happens after the gate, never before it; and redaction happens after execution, never instead of it.
A resumed call redeems its token between S3 and S4 — after quota, before validation. That single
detail is why quota exhaustion is the only failure a resume can be safely retried through, and it is
spelled out on the policy and approvals page.
Stages that fail stop the call. The stage name becomes the error field the caller sees, which is
how both SDKs choose a typed error: S3:rate-quota becomes QuotaExceeded, S5:policy becomes
PolicyDenied, S6:approval becomes ApprovalError.
Retrieving one
curl -s "$FABRIC/v1/lifecycles/qlc_000412" \
-H "Authorization: Bearer $TOKEN"
The record comes back with the tool, the resolved scope, the surface the call arrived on, the project
mode it ran under, an overall status of completed, denied, pending or failed, the start and
end times, and the ordered list of stages with each one's status, detail and timestamp.
A record belonging to another organization returns 404, not an empty record.
What the audit record holds
The audit record is append-only. Each event carries:
- who — the acting user and their role
- what — an action name such as
query-context.executed,create-ontology.policy.deniedorapproval.approved, and the target it applied to - where from — the surface (
mcp,restorsystem), and, folded into the payload, the originating client and its version and the credential or MCP connection the call arrived on - under what scope — the organization and project
- which call — the
lifecycle_id, and the trace identifier as a cross-reference - a classification — one of
Auth,Tool calls,Policy,ApprovalsorSystem - a payload, and the hashes described below
Consequential tools get the full payload. A call to a tool that only reads prose, such as
docs, is marked lightweight. When a call touched the context graph, the audit
payload also carries the list of nodes and relationships involved — their labels, identity keys and
the reference value of those keys, with the operation and the per-node verdict. Identity only: the
node's own property values are never written to the audit record.
Governance events are audited alongside tool calls. A denial writes an event of its own; requesting an approval and redeeming the resume token each write theirs, and so do approving and rejecting over the REST API. A decision made in the console's Approvals Inbox is recorded in the platform audit trail, under Governance → Audit & Lineage, together with every approval's request, use and expiry.
Retrieving it
curl -s "$FABRIC/v1/audit" \
-H "Authorization: Bearer $TOKEN"
The stream is filtered to what the caller may see. Events belonging to another organization are never returned. A project-scoped credential sees only that project's events, so one project's view can never show a sibling project's activity; an organization-scoped credential keeps the organization-wide view. Events with no organization or project — system-level ones — stay visible to everyone in the organization.
What verifying the chain actually checks
Each event is hashed, and each hash covers the hash of the event before it. Nine things go into that
hash: the event's identifier, its timestamp, the actor, the action, the target, the classification,
the lifecycle_id, the hash of the payload, and the hash of the previous event. That is what makes
tampering evident — change any one of those and the event's own hash no longer matches, and every
later hash in the chain is built on a value that has moved.
curl -s "$FABRIC/v1/audit/verify" \
-H "Authorization: Bearer $TOKEN"
{ "chain_intact": true, "broken_at": "" }
Verification walks the chain from the beginning and performs exactly three checks on every event, in this order:
- The payload hash still matches the payload. The payload is re-serialized and re-hashed and compared with the stored value.
- The link is intact. The event's recorded previous-hash must equal the hash of the event before it.
- The event hash still matches the event. The hash is recomputed over the event's identifier,
timestamp, actor, action, target, classification,
lifecycle_id, payload hash and previous hash, and compared with the stored value.
The first event that fails any of these stops the walk, and its identifier is returned as
broken_at. chain_intact is true only when every event passed all three.
Three properties of this are worth knowing precisely:
- Five recorded fields are outside the hash. The role, the surface, the organization, the project and the trace identifier are stored on the event and are not covered by it, so verification does not attest them. The trace identifier is out by design — it is a cross-reference annotation, and keeping it out means events written before the field existed still verify and that adding it can never break tamper-evidence. The other four are simply not part of the hashed set. If your attestation needs to cover which project an event belongs to, that is not something this chain proves today.
- The payload is covered, through its own hash. That includes the provenance the gateway folds into every event — the originating client, its version and the connection the call arrived on — so those genuinely are tamper-evident even though they are not named in the list above.
- Verification is not scope-filtered.
GET /v1/auditshows a caller their own slice;GET /v1/audit/verifyattests the whole chain the gateway holds. That is deliberate — a chain can only be verified as a chain — but it means the answer is about the record's integrity, not about the caller's events specifically.
Using the pair to answer a question
An auditor's question is usually of the form "did this happen, who allowed it, and can you show me it has not been edited". The three calls that answer it are:
# 1. What happened to that call, stage by stage.
curl -s "$FABRIC/v1/lifecycles/qlc_000412" -H "Authorization: Bearer $TOKEN"
# 2. Which rule decided it, and what it reasoned.
curl -s "$FABRIC/v1/policy-decisions" -H "Authorization: Bearer $TOKEN"
# 3. Whether the record it is written into is still intact.
curl -s "$FABRIC/v1/audit/verify" -H "Authorization: Bearer $TOKEN"
Every one of those is joined by the lifecycle_id the caller was handed at the start.
Where to look next
What decides a call at S5, and the full path from a gated call to a person's decision.
The same ten stages as a trace, and how to use one to find where the time went.
What S3 is enforcing, and what a caller sees when it refuses.
The boundaries the audit stream is filtered against.