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:
evidenceEvidence,plansPlans,reviewReview,progressProgress. Iconssource,modules,review,program. Entry routes/evidence/questions,/plans/plans,/review/findings,/progress/overview. Alldesigneduntil the R1 gates pass; then Evidence, Plans and Review move topreviewinforge.config.json, section 1,CLAUDE.md, the docs status page andCLAIMS-REGISTER.mdin 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 carriesillustrative = trueand an “Illustrative” label. Agent output is “agent-generated” or “proposed”. - Terminology: a Plans “protocol” is an experimental protocol;
protocols.formatismarkdown | usdm_json(defaultmarkdown); ELN “procedures” and QMS SOPs are never called protocols. - Fixed phrases (in
web/lib/content.tsconsoleCopy, 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 inusers.settings.console_run_mode.
Tenancy, identity and permissions
- Organization -> Program; scope kinds
organdprogram; every scoped record carriesorganization_id,program_id,stage_id. New organisations get programdefault; the seed addstrv-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 inweb/lib/auth/permissions.ts(PERMISSION_COUNT = 87),docs/lib/content.tsand the permissions page; sync linepermissions in sync: 87 strings (13 org, 74 program) across 2 catalogs. R3 keys afterprogram.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_owner13,org_admin12,org_member5,org_auditor6,program_owner74,program_lead73,scientist49,quality_reviewer43,manufacturing_specialist38,viewer29); only quality reviewers, leads and owners hold the oversight readsprogram.threads.readandprogram.twins.read; the scientist holds neitherevidence.hypotheses.approvenorplans.plans.approve; the viewer holdsprogram.twins.read_ownandprogram.handoffs.readand nouseormanagekey. - Identity: bearer JWT. Production RS256 with pinned issuer and audience; development HS256 with
kid: dev, accepted only whenAPP_ENVisdevortest. Re-authentication tokens: same path plusoidandtidequal to the principal,auth_timewithin 300 s, single use (utihash); deployed environments fail closed withreauth_required;scripts/dev_token.py --reauthfor development. Run tokens: RS256,kid = run-<n>, audiencetrovensa-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-builtmain.pymountssignaturesandaudit_exportafterprograms;publicis mounted beforelegacy, which stays last). - Route count: 189 designed (
EXPECTED_ROUTE_COUNT = 189;check_routes: 189 designed, 0 missingonce 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 athread_idreturns events, tool calls, interrupts, snapshot and artifacts only to a principal who may read the thread;program.agent_runs.readalone lists such runs as metadata. - Errors: RFC 7807 problem+json with
request_id;ERROR_TYPE_BASE = https://console.trovensa.com/errors;403carriespermission. Public codes added:gate_expired,policy_denied,reauth_required,plan_violation,record_signed,config_mismatch,runtime_updating,sop_external,rate_limited.POST /agent-tools/mcpreturns JSON-RPC errors, the one exception. - Streaming:
GET P/threads/{thread_id}/streamistext/event-streamwithid: <seq>and a 15 s keepalive; the API tailsagent_eventswithLISTEN(one connection per replica, 500 ms poll fallback); the web uses fetch streaming withLast-Event-ID. - Settings (phase 1 fields plus):
secrets_backend unavailable|file|aws,embedding_model_id,documents_bucket,tool_server_public_url,mcp_revision(default2025-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 throughAGENT_MODEL_IDandEMBEDDING_MODEL_ID.
Data model
- Migration
0002_consolecreates 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) addspublic_inquiries,public_rate_eventsandsite_event_counts(50 -> 53);0004_chain(R3, table only,chain_appendstill disabled) addsaudit_chain(53 -> 54) and0005_twinsthe six twin tables (54 -> 60), detailed under the R3 data model below. Enumerations aretextplus a named CHECK;alembic checkstays clean on the vector-enabled image and on plain PostgreSQL (tsvector-only fallback). - Altered:
audit_events(envelope; trigger-setentry_seq,row_hash,created_at; every FKON DELETE NO ACTION),agent_runs(thread, mode, lane, agent version, plan, usage, trace, heartbeat; new kinds andawaiting_input),sources,sops,checks,review_findings,deviations,approvals,protocols,reports,connections,tools,organizations.settings,users.settings,programs.settings;AgentCapableMixin(origin,agent_run_id,illustrative) onopen_items,experiments,sources,deviations,checks. - Audit rule: every mutation writes an audit row in the same transaction, carrying
actor_kind,reasonwhere required,interrupt_id,proposal_hashandapproved_hashfor agent writes;entry_seq,row_hashandcreated_atare set by the database;trovensa_appcan 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_hashper target type;409 record_signedafter signing;ApproveRequest {rationale, reauth_token, statement_ack}. - Retention:
agent_events.text_deltacompacted aftercomplete, deleted after 30 days;webhook_deliveries90 days (rejected 30);launch_idsswept 24 h after expiry;notifications180 days after read; threads redacted to tombstones afterthread_retention_months;customer_internaldocuments archived then purged afterdocument_hold_days;tool_calls,run_interrupts,run_snapshots,signatures,audit_eventskept for the tenant’s life. - Tone map:
TONE_STATUS_COUNT = 61(pass 12, review 16, fail 7, info 4, neutral 22; R3 addedenabled,sent,done,snoozed,revoked), pinned inweb/lib/status.tsanddocs/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_afterand five kinds;messages+mentions;artifacts.kind+persona_brief;audit_events+batch_id,twin_id,trigger_id,delegated_from_user_id(never withsignature_id);threads.schedulemigrated 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; registriesAUDIT_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.jsonwords.forbiddenand everywords.speedClaimsphrase are forbidden in site, docs and deck copy until aCLAIMS-REGISTER.mdrow exists; status copy is data (site/lib/content.ts, stampedcounts.asOf); thedesigned -> previewtransition is one commit acrossforge.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” (/securityunchanged); header CTA “Talk to us”; product fieldstageStatus: 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; auditinquiry.create; rows purged after 24 months) andPOST /public/events(204; 2 KB cap; aggregatesite_event_countsonly; no audit row per event, the one deliberate exception). Error codesrate_limited_public,consent_required,honeypot,too_fast. Settingsinquiry_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,InterruptResolvewithitems[]andInterruptResolveResult, the fixed phrases verbatim inconsoleCopy), listed in the twins reference; ownership for wave 1 in 11.5 (docs ownsdocs/**). 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
| Family | DTOs |
|---|---|
| Envelope and identity | Page[T], MeOut (user.settings), PermissionOut, PersonaOut, UserOut, OrganizationOut, ProgramOut, GroupOut, RoleAssignmentOut, AuditEventOut (envelope), AuditExportQuery, UserSettingsUpdate |
| Connections and runs | ProviderOut, 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 |
| Console | ThreadOut/ThreadCreate/ThreadUpdate/ThreadShare, MessageOut/MessageCreate/MessageFeedback, RunInterruptOut/InterruptResolve, ToolCallOut, ArtifactOut/ArtifactPromote, NotificationOut, LaunchOut, ProvenanceFooter, StreamEvent |
| Documents | DocumentOut, DocumentIngest {identifier_scheme, identifier_value} (no URL field), DocumentChunkOut |
| Evidence | QuestionOut, SourceOut (+ document_id, licence_code, provenance, cluster_key, aliases), SourceLinkOut, FindingOut, HypothesisOut, HypothesisDecision, GapOut, EvidenceAgentRunCreate |
| Plans | ResearchPlanOut, ProtocolOut (+ format, sop_version_ids, supersedes_id, signature_id), ExperimentOut, ReportOut (+ version, supersedes_id, signature_id), ToolOut (catalogue fields), ToolInvocationOut, TransitionRequest, ApproveRequest, PlansAgentRunCreate |
| Review | SopOut/SopRequirement, SopVersionOut, RequirementSetDecision, CheckOut (+ binding fields) / CheckCreate (+ sop_version_id?, binding_reason?), ReviewFindingOut (+ chunk_refs), ReviewDecision, DeviationOut (+ proposed_classification), SignatureOut (+ valid) |
| Progress | MilestoneOut, 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:
elnretired;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_libraryandanalysis_toolsstaydesigned. Adapter additionsverify_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|backgroundwith two semaphores, heartbeat every 60 s, stale re-queue atlocked_at < now - 5 min,pg_notify('agent_runs')wakes the interactive lane; job kindsingest_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_turnstreams an envelope; actionresume_interrupt {interrupt_id, decision, reason?, edits?}; event kindstext_delta,citation,tool_call_proposed,tool_call_result,interrupt,artifact_delta,state; job stateinterrupted_input_required;BootstrapIngainsthread,launch_context,tool_server_url,run_tokens[],mcp_revision; the bootstrap result reportsmodel_id_value,inference_profile_id,region; six run tokens registered withregister_secreton both sides; the system prompt says the tool server is the container’s only endpoint.mcp<2is pinned (revision2025-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
| Role | Host | Local |
|---|---|---|
| site | trovensa.com | static server on 4300 |
| docs | docs.trovensa.com | static server on 4310 |
| web | console.trovensa.com | http://localhost:3100 |
| api | api.trovensa.com | http://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