Audit ledger flow
Overview
OpenClaw Control Plane (OCC) records audit evidence as controller requests and reconciliation work run. This flow follows PostgreSQL event append, commit, and the internal State repository's list operation. It stops at the repository result; OCC has no public audit browsing API or console view. See the audit guide for the operator boundary.
Entry Points
- Trigger: a controller operation appends an event, or internal code lists audit events inside a State unit of work.
- Source:
apps/controller/src/index.ts:event - Source:
packages/occ/src/state/postgres-state.ts:PostgresPlatformState - Source:
packages/occ/src/state/postgres-work-queue.ts:INSERT_EVIDENCE_CTE_SQL - Assumptions: PostgreSQL State has a server-owned Installation; request admission and authorization are handled by the calling operation. Repository list is an internal operation, not an authorization or public serving boundary.
Flow
The diagram describes source behavior, not proof of an installed deployment.
graph TD
Request["Controller operation"] --> Factory["Create and sanitize event"]
Factory --> Append["State audit append"]
Append --> Scope{"Installation and<br/>Namespace scopes match?"}
Scope -->|yes| Encode["Encode metadata<br/>and insert row"]
Scope -->|no| Reject["Reject append"]
Encode --> Pending["Transaction-local row"]
Encode -->|reserved key or insert error| Reject
Queue["Transition in State unit"] --> QueueSQL["Insert SQL evidence"]
QueueSQL --> Pending
DirectQueue["Pool-backed stale-work recovery"] --> DirectSQL["Run two SQL statements<br/>with transition evidence"]
DirectSQL -->|each may commit| Ledger
DirectSQL -->|later statement fails| Partial["Earlier statement may<br/>already be committed"]
Pending --> Commit{"State transaction outcome"}
Commit -->|COMMIT acknowledged| Ledger[("Committed audit rows<br/>in PostgreSQL")]
Commit -->|COMMIT acknowledged| Cleanup{"Client cleanup"}
Commit -->|definite rollback| RolledBack["Discard transaction changes"]
Commit -->|unknown COMMIT| Unknown["Report unknown;<br/>do not replay automatically"]
Unknown -->|may have committed| Ledger
Cleanup -->|success| Returned["Return unit result"]
Cleanup -->|failure after COMMIT| Unknown
Reader["Internal State list"] --> Installation{"Installation exists?"}
Installation -->|no| Empty["Return empty list"]
Installation -->|yes| Rows["Read rows ordered<br/>by time and ID"]
Ledger --> Rows
Pending -->|same transaction| Rows
Rows --> Decode["Decode envelope and<br/>recognized metadata"]
Decode --> Result["Return immutable events"]
Decode -->|invalid row or JSON| Error["Reject list"]
classDef storage fill:#EDF2F7,stroke:#879AB0,color:#25364A,stroke-width:1px
classDef operation fill:#EBF3F0,stroke:#7F9D93,color:#2B4038,stroke-width:1px
classDef gate fill:#F7F1E5,stroke:#B3A078,color:#514532,stroke-width:1px
class Ledger,Rows storage
class Request,Factory,Append,Encode,Queue,QueueSQL,DirectQueue,DirectSQL,Pending,Reader,Decode,Result,Empty,Returned operation
class Scope,Reject,Commit,RolledBack,Unknown,Cleanup,Installation,Error,Partial gateExecution Trace
1. A caller constructs evidence
apps/controller/src/index.ts:event and packages/audit/src/index.ts:AuditEventFactory.create
For example, the controller creates an event for a Namespace mutation and appends it in the mutation's transaction. The factory supplies an ID, timestamp, kind and outcome defaults, validates required fields and matching Namespace scope, sanitizes details and actor fields, and freezes the event. The PostgreSQL repository does not invoke the factory or sanitize an event passed directly to append; callers of that repository must supply appropriate evidence.
2. State appends a row in the caller's transaction
packages/occ/src/state/postgres-state.ts:PostgresPlatformState,
packages/occ/src/state/postgres-state.ts:auditDetails, and
apps/controller/src/worker.ts:ControllerWorker
Append requires the event's Installation to match the initialized server-owned
Installation and its resource Namespace to equal its event Namespace. It rejects
the reserved __occAuditMetadata key in caller details. It copies ordinary
details and stores nine optional fields under that key: schemaVersion,
source, requestId, admissionDecisionId, actor, iamDriverId,
authorization, decisionReason, and reasonCode. It inserts the envelope
columns and JSON details into occ.audit_events. Database constraints and query
errors can also reject the insert.
The caller can append alongside resource changes in one unit of work. The
standalone PostgreSQL audit sink opens its own transaction. Separately, the work
queue can write reconciliation evidence directly in SQL with its transition. The
worker uses transactWithQueue for some transitions. It also calls pool-backed
stale-work recovery, which issues two separate SQL statements. Each statement is
atomic, but a later failure does not undo an earlier committed statement. A queue
using a caller-supplied client follows that client's transaction boundary. The
queue's reasonCode and attemptCount are ordinary details, not reserved metadata.
3. The transaction owner finishes or fails
packages/occ/src/state/postgres-state.ts:PostgresPlatformState and
packages/occ/src/ports/transaction.ts:RepositoryTransactionLifetime
After the callback returns, State closes admission to repository operations and waits for already admitted operations to settle before committing. On failure it attempts rollback if the transaction remains marked started, COMMIT is not uncertain, and no client error has been observed. It closes the lifetime and releases or discards the client. An acknowledged COMMIT persists the transaction, but the unit returns only after client cleanup. A lost or ambiguous COMMIT response can leave the outcome unknown: the transaction may have committed or rolled back. A cleanup failure after acknowledged COMMIT is also reported as unknown, not proof of rollback. Neither case authorizes an automatic replay. An append or list result within a unit does not by itself prove that the transaction committed.
When the checked-out client reports a transport error before callback admission, State refuses to start the callback. It refuses further repository queries and their results after an observed error, and requests client discard when that error is known before release. A transaction that fails on an observed client error reports the persistence dependency as unavailable, whatever code the error carries. If an error is observed during cleanup after an acknowledged COMMIT, State reports an unknown outcome. A caller that catches a repository error can still perform its own external effects; this guard does not control those effects or establish the outcome of an unobserved transport failure.
4. Internal list decodes the ledger
packages/occ/src/state/postgres-state.ts:auditFromRow
List returns an empty frozen array when no Installation exists. Otherwise it
reads all visible rows ordered by occurred_at, id, attaches the current
server-owned Installation ID, and decodes each row. A list on the same unit can
include its own uncommitted append. The table has no Installation ID column.
The decoder validates resource kind, outcome, kind, timestamp and object-shaped
JSON; invalid persisted data rejects the list. It removes the reserved metadata
object from details, copies only the nine recognized metadata keys, and leaves
other ordinary details in place. Unknown reserved keys cannot replace or extend
the top-level row envelope. Recognized metadata values are copied without
individual type or semantic validation or authentication, and decoding does not
re-sanitize them. The returned event
copies and array are immutable.
Debugging and Verification
- The audit guide describes the approved operator investigation path. The repository list has no filter, pagination, or public endpoint; do not treat it as a protected History API.
ScopeViolationErroron append can indicate an Installation mismatch, a Namespace mismatch, or reserved persistence metadata. Invalid persisted event values or JSON can make the entire internal list fail.tests/integration/postgres-repository-transactions.test.mjscontains PostgreSQL append, metadata decoding, rejection, and rollback cases. Those cases require the PostgreSQL test setup; their presence and this source trace do not prove a test or deployed system passed.
