Skip to Content
PlatformExplanationAbout records and signatures

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.

Last updated on