Skip to Content

Signatures

A signature is a row in signatures that binds a person, a meaning derived by the server from the route, a verbatim statement and the hash of the versioned content of one record. Signatures are reserved for approve routes (protocols, reports) and for requirement-set acceptance; approving an agent proposal in the console is not a signature. The web app labels the dialog with the fixed phrase “Decision signature (non-GxP pilot)” and prints “Signature valid” or “Record changed since signature” on the record page. Nothing here is a compliance claim; the controls are described so they can be inspected.

Record (SignatureOut)

FieldMeaning
signer_user_idnot null: agents are not users and cannot sign
printed_namethe signer’s display name at signing time
meaningauthored, reviewed, approved, acknowledged; derived by the server from the route (approve -> approved, requirement decision -> approved) and never accepted from the client
statementthe exact sentence the signer confirmed, stored verbatim: I, <printed name>, approve <record label> (record <first 16 hex of record_hash>). Decision signature (non-GxP pilot).
target_type, target_id, target_versionprotocol, report or requirement_set and the version signed
record_hashsha256 of RFC 8785 canonical JSON of the versioned content fields (below)
signed_at, auth_method, auth_evidenceidp_reauth with auth_time and a uti hash, or dev_token with a jti; the hash carries a unique index so one token signs once
reasonthe rationale from the request
validrecomputed on read: true when the current record hash equals record_hash

The row is immutable: BEFORE UPDATE OR DELETE triggers raise, the application role holds SELECT and INSERT only, and every foreign key (including signer_user_id) is ON DELETE NO ACTION, so users are deactivated, never deleted.

Record hash per target type

TargetFields hashed
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}

record_hash is sha256(JCS(fields)) with the same canonical JSON function the audit envelope uses (api/app/services/audit.py canonical_json, twin of the SQL trovensa_jcs). After a signature the service refuses update on the target with 409 record_signed; a change is a new version with supersedes_id, and the previous version keeps its signature and shows valid = true against its own content.

Requests that sign

RouteBodyMeaning
POST .../plans/protocols/{protocol_id}/approveApproveRequest {rationale, reauth_token, statement_ack}approved
POST .../plans/reports/{report_id}/approveApproveRequestapproved
POST .../review/sop-versions/{version_id}/requirements/decisionRequirementSetDecision {decision, requirements[], rationale, reauth_token, statement_ack}approved when decision = accepted
POST .../progress/approvals/{approval_id}/decisionApprovalDecision (with a signature when the target is a protocol or report)approved; approvals.signature_id links the row

statement_ack must equal the statement the API showed before re-authentication; a mismatch is rejected.

Re-authentication

  1. The dialog persists pending_signature {interrupt_id | approval_id, statement_hash, return_path, started_at} in sessionStorage, then calls the identity provider’s step-up (prompt: 'login', redirect or popup) and resumes on return.
  2. The API verifies reauth_token through the same issuer, audience and JWKS path as session tokens, and requires oid and tid equal to the session principal, auth_time within 300 s of signed_at and single use (uti hash).
  3. A token without auth_time fails closed in deployed environments with 401 reauth_required; the optional auth_time claim is enabled on the trovensa-console registration.
  4. Development mode: uv run python scripts/dev_token.py --user a@b.c --org demo --reauth mints a 300 s token with auth_time = iat and a jti; the signature records auth_method = dev_token. Deployed environments refuse dev tokens.

Where signatures appear

  • GET .../signatures/{signature_id} (program.audit.read) returns the row with valid.
  • protocols.signature_id, reports.signature_id, sop_versions.signature_id and approvals.signature_id link records to their signature; audit_events.signature_id links the audit row of the signing mutation.
  • Record pages print “Agent-generated draft; approved by <name> on <date> (approved). Signature valid.” for an agent-drafted protocol approved by a person; a later content change flips the line to “Record changed since signature”.
  • Audit exports carry signature_id beside actor_kind, approved_by_user_id, interrupt_id, proposal_hash and approved_hash.

Source: api/app/db/models/signatures.py, api/app/services/signatures.py, api/app/services/audit.py, api/app/auth/jwt.py, api/app/schemas/signatures.py, api/scripts/dev_token.py, api/alembic/versions/0002_console.py, web/lib/api/types.ts, web/lib/content.ts (consoleCopy), PRODUCT-CONTRACT.md section 10.4, decisions/0014-alcoa-envelope-and-append-only-audit.md