About records and signatures
Trovensa’s claim is narrow: it is the record of what agents proposed and what people decided. That record has to answer, months later, who did what, on whose approval, against which sources and which SOP version, and whether anything changed afterwards. This page explains how the audit trail and signatures are designed to answer those questions; it describes controls and makes no compliance claim.
The problem
Phase 1 recorded an action, a target, a user id and a diff. An agent acting on a person’s approval had no attributable identity, an edited proposal could not be told from the original, a signature meant nothing more than a status change, foreign keys quietly nulled the actor when a user was deleted, and the application owned the schema, so it could rewrite history. None of that is acceptable for a trail a reviewer will replay.
The envelope
Every audit row (audit_events) now carries an envelope: actor_kind (human, agent, system),
actor_agent_id (agent:<agent_key>@<org_id>), agent_run_id, interrupt_id, approved_by_user_id,
signature_id, reason (required for updates and deletions of records in review or approved states),
proposal_hash and approved_hash for agent writes, and occurred_at. Two hashes matter because a person
may edit a proposal before approving it: the record is written from the server-side payload, the audit row
carries both, and the record page reads “edited before approval” with the diff.
Even low-value mutations write a row (notification.read, message.feedback, user.settings_update,
thread.redact, thread.share, launch.consume, document.ingest, document.purge), so the rule “every
mutation writes an audit row in the same transaction” stays literally true and cookies hold only layout.
Why the database sets the sequence and the hash
entry_seq, row_hash and created_at are set by a BEFORE INSERT trigger; whatever the application
supplies is overwritten. row_hash is sha256 of the row’s canonical JSON (RFC 8785, produced in SQL by
trovensa_jcs) without the two hash columns; a Python twin (compute_row_hash, verify_row_hash) produces
the same bytes, and tests compare the two on twenty samples. An application-computed hash could be wrong,
inconsistent in key order, or simply skipped; a trigger cannot be. The sequence is one global sequence with
gaps allowed and order preserved, so per-organisation subsequences are monotonic. A per-organisation hash
chain (audit_chain, R2) will link entries off the request path with daily checkpoint anchors.
Why an append-only application role
Two controls make the trail unalterable. Triggers on audit_events and signatures raise on UPDATE,
DELETE and TRUNCATE; but while the application connects as the schema owner it could disable them, so the
triggers are defence in depth. The hard control is the role split: the API and worker connect as
trovensa_app, which holds SELECT and INSERT only on the audit and signature tables and cannot disable a
trigger; the owner role is used by the migrate Job alone, and the release script refuses the 0002 migration
while the api or worker DSN still names the owner. Every foreign key on these tables is ON DELETE NO ACTION, so users are deactivated and programs archived, never deleted; a test asserts that deleting a
referenced user fails.
Why signatures carry a meaning and a hash
An approval in the console is a permission exercised; a signature is more. A signatures row binds a person
(signer_user_id, not null: agents are not users), a meaning the server derives from the route (approve ->
approved), a statement stored verbatim, and record_hash, the hash of the versioned content fields of the
target (protocol {id, version, title, body_md, format, plan_id, sop_version_ids}, report {id, version, title, body_md, plan_id}, requirement set {sop_version_id, requirements}). The client never chooses the
meaning; it confirms the displayed statement. Signing needs a fresh re-authentication: the token must name
the same person and tenant, carry auth_time within 300 s, and be used once; deployed environments fail
closed when auth_time is missing. After a signature the target refuses updates (409 record_signed); a
change is a new version with supersedes_id, and the record page recomputes validity into “Signature valid”
or “Record changed since signature”. The dialog says “Decision signature (non-GxP pilot)” because that is
what it is.
Why two record systems do not collide
The QMS signs SOPs; Trovensa signs Trovensa records only. A connector SOP cannot be approved in Trovensa
(409 sop_external); what QA signs is the acceptance of the requirement set the agent extracted from it. The
ELN keeps its own review state, which Trovensa mirrors and labels “recorded in the notebook”.
What a reviewer can do with this
Export the program’s trail (GET .../audit/export) and recompute every hash; open a run’s snapshot to see
chunk ids and hashes, record ids, SOP versions, prompt version, allowlist, permission snapshot and the model
check outcome; open a signature and read its statement and validity. A quarterly restore test that
recomputes hashes and signature validity on a scratch database is the evidence the light validation package
records; a risk-based audit-trail review record is designed for R3.