Skip to Content
EngineeringSpecificationsContract summary

Contract summary

PRODUCT-CONTRACT.md at the workspace root fixes every name the seven surfaces must agree on. Names change there first, then in code. Section 10 (v2, 2026-09-14) fixes the phase-2 names and wins over sections 1 to 9 where they differ; PRODUCT-PLAN.md Part 4 explains them. This page condenses both for orientation; the contract wins wherever they differ.

Taxonomy and positioning

  • Brand: Trovensa only. Descriptor “Agentic intelligence for drug development.” Tagline “From evidence to progress.” Bare product names inside the app and the docs; the marketing prefix belongs to the site and the deck.
  • Products, in order: evidence Evidence, plans Plans, review Review, progress Progress. Icons source, modules, review, program. Entry routes /evidence/questions, /plans/plans, /review/findings, /progress/overview. All designed until the R1 gates pass; then Evidence, Plans and Review move to preview in forge.config.json, section 1, CLAUDE.md, the docs status page and CLAIMS-REGISTER.md in one commit.
  • The console is a program section (/p/[slug]/console), not a fifth product. Pilot positioning, quoted verbatim everywhere: “non-GxP decision support with a light validation package”. No compliance claims; controls are described.
  • Pillars: CONTEXT (evidence), CONTINUITY (plans, progress), RECORD (review). Stages: discovery, preclinical, clinical, manufacturing.
  • Status vocabulary: available, preview, designed, planned, roadmap. Mock data carries illustrative = true and an “Illustrative” label. Agent output is “agent-generated” or “proposed”.
  • Terminology: a Plans “protocol” is an experimental protocol; protocols.format is markdown | usdm_json (default markdown); ELN “procedures” and QMS SOPs are never called protocols.
  • Fixed phrases (in web/lib/content.ts consoleCopy, quoted by docs and deck): “Private to you. Quality reviewers and program leads can read this thread for review.”, “From a private thread”, “Paused, waiting for a decision”, “Agent-generated draft, unsigned.”, “Decision signature (non-GxP pilot)”, “Model configuration MC-n (recorded <date>)”, “recorded in the notebook”, “All N claims carry a citation”, “Signature valid”, “Record changed since signature”. Licence codes are described individually, never as “commercial-clear”. Connector docs are titled by category with vendor names in the body only.
  • Run modes: approve_each_write, auto_reads_gated_writes (default), keep_planning; remembered in users.settings.console_run_mode.

Tenancy, identity and permissions

  • Organization -> Program; scope kinds org and program; every scoped record carries organization_id, program_id, stage_id. New organisations get program default; the seed adds trv-101. Connections stay program-scoped (ADR 0018).
  • Permission catalog: 87 strings (org 13, program 27, evidence 13, plans 14, review 10, progress 10) pinned in api/app/auth/permissions.py (EXPECTED_PERMISSION_COUNT = 87), mirrored in web/lib/auth/permissions.ts (PERMISSION_COUNT = 87), docs/lib/content.ts and the permissions page; sync line permissions in sync: 87 strings (13 org, 74 program) across 2 catalogs. R3 keys after program.agent_memory.manage: program.twins.read, program.twins.read_own, program.twins.use, program.twins.manage, program.handoffs.read, program.handoffs.manage. Phase-2 keys: program.threads.read, program.threads.read_own, program.threads.use, program.threads.manage, program.documents.read, program.documents.manage, program.agent_memory.read, program.agent_memory.manage, progress.agents.use. Approving an agent write needs the write’s own permission (permission_exercised); sharing a thread is owner-only.
  • Personas: 10 at PERSONA_VERSION = 3 (org_owner 13, org_admin 12, org_member 5, org_auditor 6, program_owner 74, program_lead 73, scientist 49, quality_reviewer 43, manufacturing_specialist 38, viewer 29); only quality reviewers, leads and owners hold the oversight reads program.threads.read and program.twins.read; the scientist holds neither evidence.hypotheses.approve nor plans.plans.approve; the viewer holds program.twins.read_own and program.handoffs.read and no use or manage key.
  • Identity: bearer JWT. Production RS256 with pinned issuer and audience; development HS256 with kid: dev, accepted only when APP_ENV is dev or test. Re-authentication tokens: same path plus oid and tid equal to the principal, auth_time within 300 s, single use (uti hash); deployed environments fail closed with reauth_required; scripts/dev_token.py --reauth for development. Run tokens: RS256, kid = run-<n>, audience trovensa-agent-tools, six roles per run, worker-only private key.

API

  • Prefix /api/v1; program routes /programs/{program_id}/...; org routes /orgs/current/...; personal routes /me/...; plus /hooks/{provider_id}/{connection_id}, /launch/{launch_id} and /agent-tools/mcp. Router order: health, me, permissions, personas, providers, orgs, programs, connections, agent_runs, threads, artifacts, documents, evidence, plans, review, progress, notifications, launch, hooks, agent_tools, public, legacy (the as-built main.py mounts signatures and audit_export after programs; public is mounted before legacy, which stays last).
  • Route count: 189 designed (EXPECTED_ROUTE_COUNT = 189; check_routes: 189 designed, 0 missing once the R3 routers mount; api/scripts/designed-routes.json): 38 added in phase 2 (117 -> 155), the two public site routes in phase 3 wave 1 (155 -> 157) and the 32 twin routes of R3 (157 -> 189: inbox, brief and delegations under /me; twins, triggers, schedules, hand-offs, board and decision metrics under a program; the twin activity export). Bound-user rule: an interrupt on a twin run is resolvable only by the bound person or, for write approvals, an active delegate holding the write’s permission (403 bound_user_only). Thread rule: any agent run with a thread_id returns events, tool calls, interrupts, snapshot and artifacts only to a principal who may read the thread; program.agent_runs.read alone lists such runs as metadata.
  • Errors: RFC 7807 problem+json with request_id; ERROR_TYPE_BASE = https://console.trovensa.com/errors; 403 carries permission. Public codes added: gate_expired, policy_denied, reauth_required, plan_violation, record_signed, config_mismatch, runtime_updating, sop_external, rate_limited. POST /agent-tools/mcp returns JSON-RPC errors, the one exception.
  • Streaming: GET P/threads/{thread_id}/stream is text/event-stream with id: <seq> and a 15 s keepalive; the API tails agent_events with LISTEN (one connection per replica, 500 ms poll fallback); the web uses fetch streaming with Last-Event-ID.
  • Settings (phase 1 fields plus): secrets_backend unavailable|file|aws, embedding_model_id, documents_bucket, tool_server_public_url, mcp_revision (default 2025-06-18), run_token_jwks_ref, feature_console, sse_keepalive_seconds (15), ses_sender (notifications@trovensa.com), worker_interactive_slots (2), worker_background_slots (2), document_hold_days (30), thread_retention_months (24). Model ids only through AGENT_MODEL_ID and EMBEDDING_MODEL_ID.

Data model

  • Migration 0002_console creates 18 tables (32 -> 50): threads, messages, run_interrupts, tool_calls, artifacts, run_snapshots, notifications, agent_versions, agent_capabilities, model_configs, documents, document_chunks, connector_objects, webhook_deliveries, rate_budgets, sop_versions, signatures, launch_ids. 0003_public (phase 3 wave 1) adds public_inquiries, public_rate_events and site_event_counts (50 -> 53); 0004_chain (R3, table only, chain_append still disabled) adds audit_chain (53 -> 54) and 0005_twins the six twin tables (54 -> 60), detailed under the R3 data model below. Enumerations are text plus a named CHECK; alembic check stays clean on the vector-enabled image and on plain PostgreSQL (tsvector-only fallback).
  • Altered: audit_events (envelope; trigger-set entry_seq, row_hash, created_at; every FK ON DELETE NO ACTION), agent_runs (thread, mode, lane, agent version, plan, usage, trace, heartbeat; new kinds and awaiting_input), sources, sops, checks, review_findings, deviations, approvals, protocols, reports, connections, tools, organizations.settings, users.settings, programs.settings; AgentCapableMixin (origin, agent_run_id, illustrative) on open_items, experiments, sources, deviations, checks.
  • Audit rule: every mutation writes an audit row in the same transaction, carrying actor_kind, reason where required, interrupt_id, proposal_hash and approved_hash for agent writes; entry_seq, row_hash and created_at are set by the database; trovensa_app can only insert and read audit, chain and signature rows; audit FKs are NO ACTION, so users are deactivated and programs archived, never deleted. Low-value mutations audited: notification.read, message.feedback, user.settings_update, thread.redact, thread.share, launch.consume, document.ingest, document.purge.
  • Signatures: signer_user_id NOT NULL; meaning derived by the server; record_hash per target type; 409 record_signed after signing; ApproveRequest {rationale, reauth_token, statement_ack}.
  • Retention: agent_events.text_delta compacted after complete, deleted after 30 days; webhook_deliveries 90 days (rejected 30); launch_ids swept 24 h after expiry; notifications 180 days after read; threads redacted to tombstones after thread_retention_months; customer_internal documents archived then purged after document_hold_days; tool_calls, run_interrupts, run_snapshots, signatures, audit_events kept for the tenant’s life.
  • Tone map: TONE_STATUS_COUNT = 61 (pass 12, review 16, fail 7, info 4, neutral 22; R3 added enabled, sent, done, snoozed, revoked), pinned in web/lib/status.ts and docs/lib/content.ts.
  • R3 data model (0004_chain, 0005_twins; 53 -> 54 -> 60 tables): audit_chain (table only), twin_definitions, twin_triggers, twin_schedules, inbox_items, handoff_requests, delegations; agent_runs + twin_id, trigger_id, initiated_by, priority, target_id; run_interrupts + detached, kind + batch_approval; tool_calls + basis_hash; notifications + deliver_after and five kinds; messages + mentions; artifacts.kind + persona_brief; audit_events + batch_id, twin_id, trigger_id, delegated_from_user_id (never with signature_id); threads.schedule migrated into paused twins and dropped. Run kinds + twin_cycle, twin_handoff; tools + console.propose_handoff, console.list_handoffs, console.get_twin, console.list_decisions_waiting, plans.propose_plan_change; registries AUDIT_ACTIONS, TRIGGERABLE_ACTIONS, PAUSE_ACTIONS.

Section 11: the public site (phase 3, wave 1)

  • Posture: “private preview for design partners” is the only partner phrase; the two partner-testimony phrases listed in forge.config.json words.forbidden and every words.speedClaims phrase are forbidden in site, docs and deck copy until a CLAIMS-REGISTER.md row exists; status copy is data (site/lib/content.ts, stamped counts.asOf); the designed -> preview transition is one commit across forge.config.json, contract section 1, CLAUDE.md, the docs status page and the register once the two phase-2 measurements are registered.
  • Site names: routes /contact, /contact/sent, /company, /privacy, /terms; nav label “Security” (/security unchanged); header CTA “Talk to us”; product field stageStatus: Record<StageId, ReleaseStatus>; frames labelled “Console build, illustrative data” (screenshots) or “Illustrative” (HTML mocks); slots render nothing when empty.
  • Public API: POST /public/inquiries (202 {id, received_at}; form-encoded -> 303 /contact/sent; 8 KB cap; honeypot, consent, 3 s time gate; 5/hour and 20/day per salted address hash kept 30 days, 3/day per e-mail; audit inquiry.create; rows purged after 24 months) and POST /public/events (204; 2 KB cap; aggregate site_event_counts only; no audit row per event, the one deliberate exception). Error codes rate_limited_public, consent_required, honeypot, too_fast. Settings inquiry_notify_to, public_rate_salt, public_cors_origin.
  • Twins (R3 to R5): names fixed in 11.4 and recorded as built for R3 in 11.7 (permissions 87, personas v3 with the sizes above, routes 189, tone 61, the DTO families TwinOut, TwinTriggerOut, TwinScheduleOut, InboxItemOut, InboxResolve, PersonaBriefOut, DelegationOut, HandoffOut, BoardOut, DecisionMetricsOut, InterruptResolve with items[] and InterruptResolveResult, the fixed phrases verbatim in consoleCopy), listed in the twins reference; ownership for wave 1 in 11.5 (docs owns docs/**). Fixed phrases: “Decisions waiting for you”, “Your twin proposes. You decide.”, “Your twin never approves, signs or sends.”, “Paused. Nothing runs until you resume.”, “Expired, nothing written”, “Changed since proposed”, “Viewers have no twin. Nothing waits for you.”, “Every decision stays human and recorded. Timing claims wait for measured data.”, among the fifteen of 11.4.

DTO families

FamilyDTOs
Envelope and identityPage[T], MeOut (user.settings), PermissionOut, PersonaOut, UserOut, OrganizationOut, ProgramOut, GroupOut, RoleAssignmentOut, AuditEventOut (envelope), AuditExportQuery, UserSettingsUpdate
Connections and runsProviderOut, ConnectionOut (+ scope_rules, poll_interval_seconds, credential_class, write_policy, hook_url, api_version), SyncRunOut, ConnectorObjectOut, AgentRunOut (+ thread_id, mode, lane, agent_version_id, plan_hash, usage, trace_id), AgentEventOut, AgentVersionOut, AgentCapabilityOut, RunSnapshotOut, MemoryRecordOut
ConsoleThreadOut/ThreadCreate/ThreadUpdate/ThreadShare, MessageOut/MessageCreate/MessageFeedback, RunInterruptOut/InterruptResolve, ToolCallOut, ArtifactOut/ArtifactPromote, NotificationOut, LaunchOut, ProvenanceFooter, StreamEvent
DocumentsDocumentOut, DocumentIngest {identifier_scheme, identifier_value} (no URL field), DocumentChunkOut
EvidenceQuestionOut, SourceOut (+ document_id, licence_code, provenance, cluster_key, aliases), SourceLinkOut, FindingOut, HypothesisOut, HypothesisDecision, GapOut, EvidenceAgentRunCreate
PlansResearchPlanOut, ProtocolOut (+ format, sop_version_ids, supersedes_id, signature_id), ExperimentOut, ReportOut (+ version, supersedes_id, signature_id), ToolOut (catalogue fields), ToolInvocationOut, TransitionRequest, ApproveRequest, PlansAgentRunCreate
ReviewSopOut/SopRequirement, SopVersionOut, RequirementSetDecision, CheckOut (+ binding fields) / CheckCreate (+ sop_version_id?, binding_reason?), ReviewFindingOut (+ chunk_refs), ReviewDecision, DeviationOut (+ proposed_classification), SignatureOut (+ valid)
ProgressMilestoneOut, DependencyOut, OpenItemOut, ApprovalOut (+ signature_id), ApprovalDecision, ProgressOverviewOut, ProgressAgentRunCreate

web/lib/api/types.ts is the contract shape; every *Out carries id, created_at, updated_at.

Providers, worker and agent protocol

  • Providers: eln retired; benchling (category eln, write_policy read_only, event_verification ecdsa_jwks, credential_class service, launchable true, api_version_default v2), veeva_qualitydocs (category documents, read-only, polling, api_version_default v26.2), literature_search (platform keys, budgets platform-scoped); document_library and analysis_tools stay designed. Adapter additions verify_webhook, list_changes, fetch_object, search, render_launch, budget. All fetching runs in the API or the worker, never in the agent container.
  • Worker: lanes interactive|background with two semaphores, heartbeat every 60 s, stale re-queue at locked_at < now - 5 min, pg_notify('agent_runs') wakes the interactive lane; job kinds ingest_document, embed_chunks, purge_documents, poll_qms, reconcile_qms, sop_extract, sop_impact, notify_email, preprint_successor_check, redact_expired_threads, chain_append (R2, disabled).
  • Agent protocol (ADR 0013): phase console_turn streams an envelope; action resume_interrupt {interrupt_id, decision, reason?, edits?}; event kinds text_delta, citation, tool_call_proposed, tool_call_result, interrupt, artifact_delta, state; job state interrupted_input_required; BootstrapIn gains thread, launch_context, tool_server_url, run_tokens[], mcp_revision; the bootstrap result reports model_id_value, inference_profile_id, region; six run tokens registered with register_secret on both sides; the system prompt says the tool server is the container’s only endpoint. mcp<2 is pinned (revision 2025-06-18).

Web

Route tree /, /sign-in, /auth/bridge, /launch, /api/health, /p/[slug]/home, /p/[slug]/console and /p/[slug]/console/[threadId], the four product trees, /p/[slug]/review/sops/[sopId], /p/[slug]/setup/{overview,connections,documents,members,roles,agent-runs,agents,audit} and /org/{settings,programs,users,groups,roles,audit}. Cookies last_program, shell_panel, console_canvas, console_rail (layout only). Hooks useProgram().can(perm) and useHasOrgPermission(perm); no role checks. Gates: permissions in sync: 87 strings (13 org, 74 program), contrast pairs incl. console, check:mobile incl. /console, tone lint 61.

Hosts, secrets, ports and ownership

RoleHostLocal
sitetrovensa.comstatic server on 4300
docsdocs.trovensa.comstatic server on 4310
webconsole.trovensa.comhttp://localhost:3100
apiapi.trovensa.comhttp://localhost:8100 (database 5434 on the vector-enabled image; fake servers 8190 ELN, 8191 QMS, 8192 MCP)

Secrets: trovensa/<env>/app-db-app (application role DSN), trovensa/<env>/worker-run-token-key (worker-only), trovensa/<env>/app-run-token-jwks, trovensa/<env>/providers/literature_search; connection credentials under <org_id>/connections/<connection_id>/<key>. S3 bucket trovensa-<env>-documents; e-mail sender notifications@trovensa.com in us-east-1; database roles trovensa_app (application) and the owner role (migrate Job only); release.sh refuses 0002 while the api or worker DSN names the owner role.

Frozen after phase 0: the contract section 10, PRODUCT-PLAN.md, forge.config.json, shared/**, decisions/0013-0020 drafts, api/app/auth/{permissions,personas}.py, api/scripts/designed-routes.json, web/lib/{content,status}.ts, web/lib/auth/permissions.ts, web/lib/api/{types,endpoints}.ts. Writers: api-records, api-console, api-connectors, api-products, agent, web-console, web-shell, docs (docs/** including docs/lib/content.ts), site, deck, infra. Section 9 records the as-built deltas after each integration step; 10.10 lists the facts verified from primary sources on 2026-09-14 that override Part 4 where they differ.

Source: PRODUCT-CONTRACT.md (sections 1 to 11), PRODUCT-PLAN.md Parts 4 and 6, forge.config.json, api/app/auth/permissions.py, api/app/auth/personas.py, api/app/main.py, api/app/config.py, api/app/worker.py, api/scripts/designed-routes.json, web/lib/api/types.ts, web/lib/content.ts, web/lib/status.ts

Last updated on