On this page

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_id on 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.

StageWhat it does
S1:authenticateThe credential was verified by the transport. Recorded here so the trail is complete.
S2:resolve-scopeThe organization, department, project and role are pinned, and the tool's required grant is checked.
S3:rate-quotaThe quota buckets for the project and the organization are consumed.
S4:validate-inputThe tool checks its own arguments and reports which graph labels the call touches.
S5:policyThe policy engine returns allow, deny or allow+gate, and the decision is recorded.
S6:approvalA gated call parks here; a resumed call satisfies the gate here; an ungated call skips it.
S7:dry-runThe tool simulates the call and returns a human-readable summary of what it would do.
S8:executeThe work happens.
S9:post-processThe result is shaped and redacted before it leaves.
S10:recordThe 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.denied or approval.approved, and the target it applied to
  • where from — the surface (mcp, rest or system), 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, Approvals or System
  • 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:

  1. The payload hash still matches the payload. The payload is re-serialized and re-hashed and compared with the stored value.
  2. The link is intact. The event's recorded previous-hash must equal the hash of the event before it.
  3. 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/audit shows a caller their own slice; GET /v1/audit/verify attests 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