Technical
Agent-Native Design
How AI agents work alongside attorneys as first-class practitioners.
Loading…
Technical
How AI agents work alongside attorneys as first-class practitioners.
Legal work is increasingly mediated by AI. The question is not whether agents will participate in legal workflows, but how. Most legal tech platforms treat AI as a feature layer: a chatbot bolted onto a document viewer, a summarization button added to a search screen. These integrations are brittle: every new capability requires new UI, new endpoints, new code paths.
Intactus takes a different approach. The platform is designed so that every action a human can take, an agent can also take, and every action an agent takes is auditable, privilege-safe, and attributable to the directing attorney. Agents are not a future milestone feature; they are a structural property of the architecture.
This means:
agent_owner_id). The attorney directs, the agent executes, and the audit trail proves it. This is Heppner-safe by design.Implementation Status: Not yet implemented. Report templates, the execution engine, and named report types are planned for the court-output milestone.
Report templates define the tool call sequences and synthesis prompts that produce attorney work product. The architecture evolves across phases:
Template execution engine: A thin orchestrator that receives a template identifier and case context, executes the defined tool call sequence (e.g., evidence.search then facts.list then relationships.traverse), collects all tool results, and passes them to a synthesis prompt that produces the final report. The orchestrator owns sequencing and error handling; the template owns domain logic.
Error handling: If a tool call fails during template execution, the template defines fallback behavior per step: skip the step and note the gap in the report, retry with broader parameters, or abort the report and surface the error to the attorney. Partial results are preserved so the attorney can see what succeeded.
Every operation available to a human user through the UI is available to an agent through the API. If an attorney can search evidence, an agent can search evidence with the same parameters, the same filters, and the same result format. No capability exists only in the UI.
Why this matters: Parity ensures agents are not constrained to pre-built features. An agent working a case has the same operational vocabulary as an attorney.
Every entity in the system (evidence items, facts, claims, issues (claims and issues are planned for a future milestone), entities, relationships, timeline events, reports) is exposed as atomic CRUD operations. Agents compose these primitives into complex workflows. The ingestion pipeline, for example, is not a monolithic function. Each step (extract text, extract entities, classify, summarize, extract facts) is an individually callable tool.
Why this matters: Granularity enables agents to perform partial operations, retry individual steps, and compose novel workflows that weren't anticipated at design time.
Complex operations are compositions of atomic tools, not monolithic procedures. A "Case Brief" report is a prompt template that calls evidence.search, facts.list, issues.get, and relationships.traverse, then synthesizes the results. A new report type requires a new prompt template and zero new code.
Why this matters: Composability means the platform's capabilities grow with the number of tools and prompts, not with the number of hardcoded features.
Agents discover available tools at runtime by reading the OpenAPI spec. When a practice-area module registers new endpoints (e.g., a family law module adds /family-law/detect-custody-violations), agents immediately discover and use them. No agent code changes required.
Why this matters: The platform becomes more capable over time through endpoint registration, not through agent redesign. Domain expertise is encoded in tools and prompts, not in agent logic.
Agents can see everything relevant to their work (current case state, recent changes, event streams) at any time, not just at session start. A static snapshot is insufficient; agents need live context to make good decisions.
Why this matters: Without observability, agents operate on stale data and make decisions based on outdated state. Observability ensures agent actions are grounded in current reality.
The single most important architectural decision in Intactus:
Every FastAPI endpoint IS a tool. The OpenAPI spec IS the tool registry. There is no separate "agent API."
Every endpoint is a tool. When a developer adds a new FastAPI route, they've added a new tool. There is no separate "tool layer" to maintain; the API is the tool layer.
The OpenAPI spec is the tool registry. Agents discover available operations by reading the OpenAPI spec. Tool metadata (permissions, audit category, entity type) is encoded in OpenAPI extensions (x-tool-*) on each endpoint.
The UI calls the same endpoints agents call. The Next.js frontend is a thin client. No BFF (backend-for-frontend). No screen-specific endpoints. If the UI needs data in a specific shape, it composes multiple API calls client-side. The Next.js frontend calls the API using standard fetch() requests. An auto-generated TypeScript client from the OpenAPI spec is planned but not yet implemented.
Parity is automatic. The question is never "did we remember to expose this to agents?" It is "did we build the API correctly?" Every new endpoint automatically has a tool equivalent because it IS a tool.
Every FastAPI route carries tool metadata via OpenAPI extensions:
/cases/{case_id}/evidence:
get:
x-tool-name: evidence.list
x-tool-permission: read:evidence
x-tool-audit-category: search
x-tool-entity-type: evidenceEvery FastAPI route should declare four OpenAPI extensions: x-tool-name, x-tool-permission, x-tool-audit-category, and x-tool-entity-type. The tool_meta() helper function provides consistent metadata, and a unit test validates the helper's output format. Full CI enforcement across all application routes is planned.
The question is never "did we remember to expose this to agents?" It is "did we build the API correctly?" Parity is an automatic consequence of the architecture, not a separate goal.
Standards that apply to every endpoint in the platform.
All .list endpoints use cursor-based pagination:
GET /cases/{id}/evidence?cursor={opaque_cursor}&limit=50
→ { items: [...], next_cursor: "abc123", has_more: true }
limit: 50. Max: 100.next_cursor is opaque; clients don't parse it. Pass it as cursor on the next request.All POST (create) endpoints accept an Idempotency-Key header:
POST /cases/{id}/evidence
Idempotency-Key: client-generated-uuid
/v1/cases/{id}/evidenceSunset header on responses.How agents get credentials, connect, and operate.
Attorneys generate API keys for agents through the dashboard. Each key is scoped:
| Scope | Description |
|---|---|
agent_owner_id |
The attorney who owns this key. All agent actions are attributed to this attorney. |
allowed_cases |
Which cases the agent can access (subset of the attorney's assigned cases). |
operation_permissions |
Permitted operation types: read, write, delete, analyze. |
rate_limits |
Per-key rate limits (requests/min, requests/hour). |
Agents authenticate with Authorization: Bearer <api_key>. The same middleware that validates human bearer tokens validates agent keys, with additional agent-specific permission checks (case scope, operation type, entity type).
Agent sessions provide scoped, time-limited work contexts:
POST /agent/sessions with agent type, target case(s), requested permissionssession_id + case briefing (see Observability)PATCH /agent/sessions/{id}.DELETE /agent/sessions/{id}, or automatic on TTL expiry.Implementation Status: Not yet implemented. Automation rules and standing instructions are planned for a future milestone.
Attorneys configure automation rules that authorize agents to act reactively. An automation rule is:
{
"rule_id": "uuid",
"agent_type": "intake",
"trigger": "event:evidence.created",
"case_ids": ["uuid", "..."], // or null for all cases
"action": "Start intake session, run triage on new evidence",
"enabled": true,
"created_by": "attorney_uuid"
}When a matching event fires, the platform creates an agent session on the attorney's behalf and starts the configured agent. The attorney can view, pause, and delete automation rules through the dashboard.
This resolves the tension between "no independent action" and reactive agents: the Intake Agent doesn't act independently. It acts on standing instructions the attorney explicitly configured. Every automated session traces back to a specific automation rule created by a specific attorney.
API keys are long-lived credentials; sessions are short-lived work contexts. An agent MUST create a session before calling any tool endpoint (except tools.list and tools.search). The session scopes the agent's access for a specific work unit:
POST /agent/sessions. All tool calls require a valid session.This separation enables: key rotation without disrupting active work, per-session case scoping (narrower than the key's allowed_cases), per-session webhook registration, and session-level usage tracking.
The agent role sits in the permission system alongside existing roles:
| Role | Sees | Can Do |
|---|---|---|
| Firm admin | All cases in firm | Manage attorneys, billing, firm settings |
| Attorney | Assigned cases | Full evidence management, AI analysis, case administration |
| Staff | Assigned cases | Evidence intake, organization, search (no AI analysis) |
| Agent | Cases assigned to owner attorney | API operations scoped by permission grants |
| Client | Own case only | Upload evidence, portal features, Chat with My Case, secure messaging |
| Guest | Specific case(s) only | Read-only access to attorney dashboard view |
Every agent operates within strict boundaries:
agent_owner_id: The attorney who authorized the agent. Every agent action is attributable to this attorney.read, write, delete, analyze. A Research Agent may have read + analyze; an Intake Agent may have read + write.The
analyzepermission is required for any operation that invokes an LLM beyond ingestion-time processing. Specifically:reports.generate,issues.suggest_structure,relationships.traverse(multi-hop only), all Deep Analysis operations (M11+), and Chat with My Case queries. Ingestion-time LLM calls (classify, summarize, extract_facts) requirewritepermission on evidence, notanalyze, because they are data enrichment, not analysis.
Client visibility boundary: Evidence items have a
client_visibleflag (default:truefor client-uploaded evidence,falsefor attorney-added work product). The Client-Facing Agent's entity type restriction limits it to records whereclient_visible = true. Attorneys toggle visibility per evidence item. Facts inherit visibility from their source evidence item unless overridden.
Under the Kovel doctrine and post-Heppner analysis, attorney-client privilege extends to agents (human or AI) acting at the attorney's direction. Intactus enforces this structurally:
agent_owner_id linking it to the directing attorneyThis is not a policy; it is a data model constraint.
Every user action in the platform maps to a specific API endpoint (tool). Organized by domain.
M1 = implemented and available now. Future = planned for a later milestone.
All .list endpoints accept cursor and limit query parameters per API Conventions. All .search endpoints accept the same pagination parameters in the request body.
| Tool | Description |
|---|---|
request_magic_link |
Request a magic link (always returns 200) |
verify_magic_link |
Verify magic link token, create session |
logout |
Revoke current session |
list_sessions |
List current user's active sessions |
revoke_session |
Revoke a specific session |
| Tool | Description |
|---|---|
firms.get |
Get the authenticated user's firm |
firms.update |
Update firm settings (firm_admin only) |
| Tool | Description |
|---|---|
users.me |
Get authenticated user's profile |
users.list |
List users in the firm (paginated) |
users.get |
Get a user by ID |
users.create |
Invite a new user (firm_admin only, sends magic link) |
users.update |
Update user profile |
users.deactivate |
Deactivate a user (firm_admin only, soft delete) |
| Tool | Description |
|---|---|
cases.create |
Create a new case |
cases.get |
Get case details with participants |
cases.list |
List cases (filtered by permission) |
cases.update |
Update case metadata |
cases.archive |
Archive a case |
cases.add_participant |
Add attorney, staff, or client to case |
cases.remove_participant |
Remove participant from case |
Cases cannot be deleted, only archived. Archived cases remain accessible for data retention compliance.
For exact endpoint URLs, see the OpenAPI spec at
/api/docs.
Future (M2+):
| Tool | Endpoint | Description |
|---|---|---|
cases.get_intake_email |
GET /cases/{id}/intake-email |
Get the case-specific intake email address |
cases.configure |
PATCH /cases/{id}/settings |
Update case settings (expiry, intake email, etc.) |
| Tool | Endpoint | Description |
|---|---|---|
evidence.list |
GET /cases/{id}/evidence |
List evidence items with filters |
evidence.get |
GET /evidence/{id} |
Get evidence item details |
evidence.create |
POST /cases/{id}/evidence |
Create evidence metadata record |
evidence.update |
PATCH /evidence/{id} |
Update evidence metadata |
evidence.delete |
DELETE /evidence/{id} |
Delete evidence item |
evidence.search |
POST /cases/{id}/evidence/search |
Full-text + structured search |
evidence.upload |
POST /cases/{id}/evidence/upload |
Initiate upload (returns presigned URL) |
evidence.confirm_upload |
POST /evidence/uploads/{id}/confirm |
Confirm upload complete, trigger ingestion |
evidence.download |
GET /evidence/{id}/download |
Get presigned download URL |
evidence.get_processing_status |
GET /evidence/{id}/processing-status |
Check ingestion pipeline status |
Additional implemented endpoints not listed above: enrichment (POST /evidence/{id}/enrich, POST /evidence/{id}/cancel-enrichment), re-extraction (POST /evidence/{id}/re-extract, POST /evidence/{id}/extract-entities, POST /evidence/{id}/summarize, POST /evidence/{id}/extract-facts), inline view (GET /evidence/{id}/view).
| Tool | Endpoint | Description |
|---|---|---|
facts.list |
GET /cases/{id}/facts |
List facts with filters (supports search via query params) |
facts.get |
GET /facts/{id} |
Get fact details including all fact_evidence rows (source snippets, offsets, is_primary flag) |
facts.create |
POST /cases/{id}/facts |
Create a fact with one or more evidence sources; each source creates a fact_evidence row |
facts.update |
PATCH /facts/{id} |
Update fact text or metadata |
facts.delete |
DELETE /facts/{id} |
Hard-delete a fact |
facts.bulk_update |
POST /cases/{id}/facts/batch-update |
Batch update fact status (approve, dismiss, or revert) |
facts.update_citation |
PATCH /facts/{id}/citation |
Update a fact's citation text |
facts.link_entity |
POST /facts/{id}/entities |
Link an entity to a fact |
facts.unlink_entity |
DELETE /facts/{id}/entities/{entity_id} |
Unlink an entity from a fact |
facts.add_temporal_mention |
POST /facts/{id}/temporal-mentions |
Add a temporal mention to a fact |
facts.delete_temporal_mention |
DELETE /facts/{id}/temporal-mentions/{mention_id} |
Remove a temporal mention from a fact |
facts.bulk_link_entities |
POST /cases/{id}/facts/batch-link-entity |
Batch link entities to facts |
facts.bulk_link_temporal |
POST /cases/{id}/facts/batch-link-temporal |
Batch link temporal mentions to facts |
parse_date |
POST /parse-date |
Parse a natural language date string into structured date components |
Facts can be hard-deleted via DELETE /facts/{id} or status-changed to dismissed via bulk update.
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
claims.create |
POST /cases/{id}/claims |
Create a claim |
claims.get |
GET /claims/{id} |
Get claim details |
claims.list |
GET /cases/{id}/claims |
List claims in a case |
claims.update |
PATCH /claims/{id} |
Update claim title/description |
claims.delete |
DELETE /claims/{id} |
Delete a claim |
claims.reorder |
PATCH /cases/{id}/claims/reorder |
Reorder claims |
issues.create |
POST /claims/{id}/issues |
Create an issue under a claim |
issues.get |
GET /issues/{id} |
Get issue details |
issues.list |
GET /claims/{id}/issues |
List issues in a claim |
issues.update |
PATCH /issues/{id} |
Update issue title/description |
issues.delete |
DELETE /issues/{id} |
Delete an issue |
issues.reorder |
PATCH /claims/{id}/issues/reorder |
Reorder issues within a claim |
issues.link_fact |
POST /issues/{id}/facts/{fact_id} |
Link a fact to an issue |
issues.unlink_fact |
DELETE /issues/{id}/facts/{fact_id} |
Unlink a fact from an issue |
issues.link_evidence |
POST /issues/{id}/evidence/{evidence_id} |
Link evidence to an issue |
issues.unlink_evidence |
DELETE /issues/{id}/evidence/{evidence_id} |
Unlink evidence from an issue |
issues.suggest_structure |
POST /cases/{id}/issues/suggest |
AI-suggest issue structure from facts |
| Tool | Endpoint | Description |
|---|---|---|
entities.create |
POST /cases/{id}/entities |
Manually create an entity |
entities.get |
GET /entities/{id} |
Get entity details |
entities.list |
GET /cases/{id}/entities |
List extracted entities (supports search via query params) |
entities.update |
PATCH /entities/{id} |
Update entity metadata |
entities.delete |
DELETE /entities/{id} |
Delete an entity |
entities.merge |
POST /entities/{id}/merge |
Merge duplicate entities |
entities.unmerge |
POST /entities/{id}/unmerge |
Unmerge a previously merged entity |
entities.summarize |
POST /entities/{id}/summarize |
Generate an AI summary for an entity |
entities.get_facts |
GET /entities/{id}/facts |
List facts linked to an entity |
entities.dedup_suggestions |
GET /cases/{id}/entities/dedup-suggestions |
Get dedup suggestions (simple) |
entities.llm_dedup_suggestions |
GET /cases/{id}/entities/llm-dedup-suggestions |
Get LLM-powered dedup suggestions |
entities.list_dedup_suggestions |
GET /cases/{id}/dedup-suggestions |
List dedup suggestions with filtering |
entities.dedup_count |
GET /cases/{id}/dedup-suggestions/count |
Count pending dedup suggestions |
entities.dedup_clusters |
GET /cases/{id}/dedup-suggestions/clusters |
Get dedup suggestion clusters |
entities.dedup_comparison |
GET /cases/{id}/dedup-suggestions/{suggestion_id}/comparison |
Get detailed comparison for a dedup suggestion |
entities.dedup_approve |
POST /cases/{id}/dedup-suggestions/{suggestion_id}/approve |
Approve a dedup suggestion (merge) |
entities.dedup_reject |
POST /cases/{id}/dedup-suggestions/{suggestion_id}/reject |
Reject a dedup suggestion |
entities.dedup_bulk |
POST /cases/{id}/dedup-suggestions/bulk |
Bulk approve/reject dedup suggestions |
entities.dedup_sweep |
POST /cases/{id}/dedup-sweep |
Trigger a dedup sweep for a case |
entities.auto_merges |
GET /cases/{id}/auto-merges |
List auto-merge history |
| Tool | Endpoint | Description |
|---|---|---|
relationships.create |
POST /cases/{id}/relationships |
Create a relationship |
relationships.get |
GET /relationships/{id} |
Get relationship details |
relationships.list |
GET /cases/{id}/relationships |
List relationships |
relationships.update |
PATCH /relationships/{id} |
Update a relationship |
relationships.delete |
DELETE /relationships/{id} |
Delete a relationship |
relationships.get_facts |
GET /relationships/{id}/facts |
List facts linked to a relationship |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
reports.generate |
POST /cases/{id}/reports |
Generate a report (async, returns job_id) |
reports.get |
GET /reports/{id} |
Get report metadata |
reports.list |
GET /cases/{id}/reports |
List reports for a case |
reports.get_status |
GET /reports/{id}/status |
Check generation status |
reports.download |
GET /reports/{id}/download |
Get presigned download URL |
reports.delete |
DELETE /reports/{id} |
Delete a report |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
exports.create |
POST /cases/{id}/exports |
Create an export (format + scope, async) |
exports.get_status |
GET /exports/{id}/status |
Check export status |
exports.download |
GET /exports/{id}/download |
Get presigned download URL |
exports.list |
GET /cases/{id}/exports |
List exports for a case |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
messages.create |
POST /cases/{id}/messages |
Send a message in a case |
messages.list |
GET /cases/{id}/messages |
List messages in a case |
messages.get |
GET /messages/{id} |
Get a message |
messages.get_thread |
GET /messages/{id}/thread |
Get full message thread |
messages.search |
POST /cases/{id}/messages/search |
Search messages by text, sender, date range |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
chat.query |
POST /cases/{id}/chat |
Submit a client question. Returns answer with evidence citations. Requires client or client_facing_agent role. |
chat.history |
GET /cases/{id}/chat |
Get chat history for a case (client-scoped) |
Note: chat.query is the tool the Client-Facing Agent calls. It performs question classification (factual vs. legal advice), agentic retrieval over client-visible evidence, response generation with citations, and guardrail enforcement as a single composite endpoint. This is an intentional exception to the "one operation per endpoint" principle: the chat UX requires a single round-trip for responsiveness.
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
notifications.create |
POST /notifications |
Create a notification |
notifications.list |
GET /notifications |
List notifications for current user |
notifications.mark_read |
POST /notifications/{id}/read |
Mark notification as read |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
monitoring.create |
POST /cases/{id}/monitors |
Create a monitoring job |
monitoring.list |
GET /cases/{id}/monitors |
List monitors for a case |
monitoring.get |
GET /monitors/{id} |
Get monitor details |
monitoring.pause |
POST /monitors/{id}/pause |
Pause a monitor |
monitoring.resume |
POST /monitors/{id}/resume |
Resume a paused monitor |
monitoring.delete |
DELETE /monitors/{id} |
Delete a monitor |
monitoring.get_results |
GET /monitors/{id}/results |
Get monitoring results |
monitoring.search |
POST /cases/{id}/monitors/search |
Search monitors by URL, status, type |
monitoring.update |
PATCH /monitors/{id} |
Update monitor URL, schedule, or configuration |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
designations.create |
POST /transcripts/{id}/designations |
Mark a page:line range for trial |
designations.list |
GET /transcripts/{id}/designations |
List designations for a transcript |
designations.import_opposing |
POST /transcripts/{id}/designations/import |
Import opposing counsel's designations |
designations.add_counter |
POST /designations/{id}/counter |
Add counter-designation |
designations.update_status |
PATCH /designations/{id} |
Update designation status |
designations.export |
POST /transcripts/{id}/designations/export |
Export designation report |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
transcripts.upload |
POST /cases/{id}/transcripts |
Upload deposition transcript (PDF/text) |
transcripts.get |
GET /transcripts/{id} |
Get transcript with page:line structure |
transcripts.list |
GET /cases/{id}/transcripts |
List transcripts in a case |
transcripts.search |
POST /cases/{id}/transcripts/search |
Full-text search across transcripts |
transcripts.get_citations |
GET /transcripts/{id}/citations |
Get Bluebook citations for marked passages |
transcripts.link_exhibit |
POST /transcripts/{id}/exhibits/{evidence_id} |
Link transcript exhibit reference to evidence |
transcripts.unlink_exhibit |
DELETE /transcripts/{id}/exhibits/{evidence_id} |
Unlink exhibit |
transcripts.update |
PATCH /transcripts/{id} |
Update transcript metadata |
transcripts.delete |
DELETE /transcripts/{id} |
Delete a transcript |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
filters.create |
POST /cases/{id}/filters |
Save a filter configuration |
filters.list |
GET /cases/{id}/filters |
List saved filters |
filters.get |
GET /filters/{id} |
Get filter details |
filters.apply |
POST /filters/{id}/apply |
Apply a saved filter. Returns { result_type: "evidence"|"facts"|"timeline", items: [...], total_count, next_cursor }. The result_type is determined by the filter's target view. |
filters.delete |
DELETE /filters/{id} |
Delete a saved filter |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
timeline.query |
POST /cases/{id}/timeline |
Query composite timeline (filters: date range, entity, source type, classification, issue). Returns { events: [{ evidence_id, date, source_type, entity_ids, classifications, summary }], total_count, date_range: { min, max } }. Events are evidence items projected into timeline format with denormalized metadata for rendering. |
| Tool | Endpoint | Description |
|---|---|---|
ingestion.extract_text |
POST /evidence/{id}/extract-text |
Extract text from evidence |
ingestion.extract_entities |
POST /evidence/{id}/extract-entities |
Extract entities from text |
ingestion.extract_relationships |
POST /evidence/{id}/extract-relationships |
Extract relationships |
ingestion.summarize |
POST /evidence/{id}/summarize |
Generate summary |
ingestion.extract_facts |
POST /evidence/{id}/extract-facts |
Extract facts with citations |
Note: Hashing and manifest recording happen automatically during the upload confirmation step — they are not separate endpoints. Classification is not yet implemented as a standalone endpoint.
| Tool | Endpoint | Description |
|---|---|---|
jobs.get_status |
GET /jobs/{id} |
Get job status and progress |
jobs.list |
GET /jobs |
List jobs (filterable by case, type, status) |
jobs.cancel |
POST /jobs/{id}/cancel |
Cancel an in-flight job |
jobs.retry |
POST /jobs/{id}/retry |
Retry a failed job |
jobs.get_result |
GET /jobs/{id}/result |
Get job output (or presigned download URL) |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
audit.list |
GET /cases/{id}/audit |
List audit entries for a case |
audit.get |
GET /audit/{id} |
Get audit entry details |
audit.search |
POST /cases/{id}/audit/search |
Search audit log |
audit.export |
POST /cases/{id}/audit/export |
Export audit trail |
| Tool | Endpoint | Description |
|---|---|---|
agents.create_session |
POST /agent/sessions |
Create an agent session |
agents.get_session |
GET /agent/sessions/{id} |
Get session details |
agents.terminate_session |
DELETE /agent/sessions/{id} |
Terminate a session |
agents.list_permissions |
GET /agent/permissions |
List current agent's permissions |
agents.get_briefing |
GET /agent/sessions/{id}/briefing |
Get/refresh case briefing |
Implementation Status: Not yet implemented. A basic GET /usage endpoint exists for internal tracking, but the full billing infrastructure described below is not built.
| Tool | Endpoint | Description |
|---|---|---|
usage.get_case_summary |
GET /cases/{id}/usage |
Per-case usage: operations breakdown, total cost, included budget consumed, overage, manual-equivalent savings |
usage.get_firm_summary |
GET /usage |
Firm-level: all cases aggregated, included budget remaining, overage, period totals |
usage.get_agent_summary |
GET /agent/sessions/{id}/usage |
Usage for a specific agent session |
usage.list_operations |
GET /cases/{id}/usage/operations |
Itemized list of AI operations on a case (filterable by date, type, agent) |
usage.tag_billable |
PATCH /usage/operations/{id} |
Tag an operation as client-billable or firm-absorbed |
usage.export_client_report |
POST /cases/{id}/usage/export |
Generate client-facing cost export (PDF/CSV) with professional labels. Async, returns job_id. |
| Tool | Endpoint | Description |
|---|---|---|
tools.list |
GET /openapi.json |
Full OpenAPI spec (the tool registry) |
tools.search |
GET /tools/search?q={query} |
Search tools by name/description. Convenience endpoint that searches the OpenAPI spec's x-tool-name and description fields. Not a separate index; the OpenAPI spec is the single source of truth. |
How agents react to changes in the platform. Two delivery mechanisms support different agent architectures.
Implementation Status: Not yet implemented. Only polling-based event consumption is available.
For agents that can receive HTTP callbacks:
PATCH /agent/sessions/{id})GET /agent/sessions/{id}/dead-letters)Every webhook delivery includes an X-Intactus-Signature header:
X-Intactus-Signature: sha256=<HMAC-SHA256 of payload using webhook secret>
The webhook secret is generated when the agent registers its webhook URL. Agents MUST verify this signature before processing events to prevent spoofed payloads.
For simpler agents that pull rather than receive:
GET /events?since={timestamp}&types={event_types}: returns all events since the given timestamp?types=evidence.created,fact.created?wait=30 holds the connection up to 30 seconds if no events are available, reducing chattinessEvents returned by the polling endpoint are filtered by the caller's access permissions. Agents only see events for cases in their session's
case_ids. Human users only see events for their assigned cases. There is no way to poll for cross-case or cross-firm events.
21 event types are currently implemented:
| Event | Trigger |
|---|---|
evidence.created |
New evidence item added to case |
evidence.updated |
Evidence item metadata updated |
evidence.deleted |
Evidence item deleted |
evidence.reviewed |
Evidence item marked as reviewed |
evidence.re_extracted |
Evidence item re-extracted |
evidence.viewed |
Evidence item viewed inline |
evidence.processed |
Ingestion/enrichment pipeline completed for an evidence item |
case.deleted |
Case deleted |
job.completed |
Background job completed successfully |
job.failed |
Background job failed |
entity.created |
New entity extracted or manually created |
entity.updated |
Entity metadata updated |
entity.deleted |
Entity deleted |
entity.merged |
Duplicate entities were merged |
entity.unmerged |
Previously merged entity was unmerged |
entity.auto_merged |
Entities automatically merged during enrichment |
relationship.created |
New relationship created |
relationship.updated |
Relationship updated |
relationship.deleted |
Relationship deleted |
fact.created |
New fact extracted or manually created |
fact.updated |
Fact text, status, or metadata updated |
fact.deleted |
Fact deleted |
{
"event_id": "uuid",
"event_type": "evidence.processed",
"case_id": "uuid",
"entity_type": "evidence",
"entity_id": "uuid",
"actor_type": "human|agent|system",
"actor_id": "uuid",
"timestamp": "ISO8601",
"data": { }
}The data field contains event-specific payload (e.g., for evidence.processed, it includes the processing results and any extracted metadata).
How agents handle long-running tasks. Any operation that takes more than 5 seconds returns a job_id instead of blocking.
Agent calls tool that triggers async work (e.g., reports.generate)
↓
API returns immediately:
{
"job_id": "uuid",
"status": "queued",
"poll_url": "/jobs/{id}"
}
↓
Agent polls job status OR receives webhook on completion
↓
On completion: jobs.get_result(job_id) returns the output
- For file outputs: returns a presigned download URL
- For data outputs: returns the data inline
queued → processing → completed
→ failed
cancelling → cancelled
Failed jobs include actionable information:
{
"job_id": "uuid",
"status": "failed",
"error": {
"code": "processing_error",
"message": "OCR failed on page 3 of document",
"retry_guidance": "Retry with lower resolution or skip page 3",
"partial_results": {
"pages_processed": 2,
"pages_failed": 1
}
}
}Agents can cancel in-flight jobs via POST /jobs/{id}/cancel. Jobs that have already completed cannot be cancelled.
How agents upload and download files. All file transfers use presigned S3 URLs; agents never send file contents through the API.
1. Agent calls evidence.upload(case_id, filename, content_type, size_bytes)
↓
2. API returns:
{
"upload_url": "https://s3.amazonaws.com/...",
"upload_id": "uuid",
"expires_in": 3600
}
↓
3. Agent PUTs file directly to S3 via presigned URL
↓
4. Agent calls evidence.confirm_upload(upload_id) to trigger ingestion
↓
5. API returns { "job_id": "uuid" } for pipeline tracking
1. Agent calls evidence.download(evidence_id) or exports.download(export_id)
↓
2. API returns:
{
"download_url": "https://s3.amazonaws.com/...",
"expires_in": 3600,
"content_type": "application/pdf",
"size_bytes": 1048576
}
↓
3. Agent GETs file from presigned URL
Implementation Status: Not yet implemented. Batch upload endpoints are planned. Currently, agents upload files one at a time using the single-file upload flow above.
Every tool returns errors in a consistent, machine-readable format, so agents can handle errors programmatically without parsing human-readable messages.
{
"error": {
"code": "FORBIDDEN",
"message": "Agent does not have 'write:facts' permission on case {case_id}",
"details": {
"required_permission": "write:facts",
"agent_permissions": ["read:evidence", "read:facts"],
"case_id": "uuid"
},
"retry_after": null,
"suggestion": "Request additional permissions from the directing attorney"
}
}Error codes are UPPERCASE strings. The following codes are in use or reserved:
| Code | HTTP Status | Meaning |
|---|---|---|
VALIDATION_ERROR |
422 | Request body or parameters failed validation |
NOT_FOUND |
404 | Entity does not exist or is not accessible |
FORBIDDEN |
403 | Caller lacks required permission or scope |
UNAUTHORIZED |
401 | Missing or invalid authentication token |
RATE_LIMITED |
429 | Rate limit exceeded (check retry_after) |
CONFLICT |
409 | Concurrent modification or duplicate conflict |
INTERNAL_ERROR |
500 | Unexpected server error (includes correlation_id in details) |
QUOTA_EXCEEDED |
429 | Monthly AI operations cost cap or session limit reached (M2+) |
IDEMPOTENCY_BODY_MISMATCH |
422 | Idempotency key reused with different request body |
IDEMPOTENCY_CONFLICT |
409 | Concurrent request with same idempotency key in progress |
Every error includes:
retry_after in seconds, or null if not retryable)Protection against runaway agents and cost overruns.
Configurable by the attorney when generating the key:
| Limit | Default | Configurable |
|---|---|---|
| Requests per minute | 100 | Yes |
| Requests per hour | 10,000 | Yes |
| Concurrent requests | 10 | Yes |
| Quota | Default | Configurable |
|---|---|---|
| Max concurrent agent sessions per case | 5 | Yes |
| Max evidence items per upload batch | 100 | Yes |
Agent operations consume the same per-operation rate card as human-initiated operations. There is no separate "agent tier." When an Intake Agent classifies evidence, it's billed the same $0.15 per item as when an attorney triggers classification. See business-model.md for the full rate card.
Attorney-configurable cost controls:
| Control | Description | Default |
|---|---|---|
| Per-case monthly cap | Hard spending limit. Operations fail with quota_exceeded when reached. |
None (attorney sets) |
| Per-case alert threshold | Soft warning notification at configurable amount. No interruption. | 80% of cap (if cap set) |
| Firm monthly cap | Overall AI budget ceiling across all cases. | None (attorney sets) |
| Per-operation approval | Optional: require attorney confirmation before expensive operations. | Off |
When a cap is reached:
quota_exceeded error (429) with details.cap_type, details.current_spend, details.cap_amountsuggestion: "Per-case monthly cap reached. Contact directing attorney to increase the cap."The Vault + Analysis subscription tier includes a monthly AI operations budget per attorney. Usage tracking reflects: operations consumed, included budget remaining, and overage amount. See business-model.md for tier details.
Implementation Status: Not yet implemented. The approval gate and awaiting_approval/denied job states are planned for a future milestone.
When enabled on a case, expensive operations (configurable threshold) would require attorney confirmation before proceeding.
If an agent hits 50 consecutive errors within a 5-minute window:
Every API response includes:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1708534800
How we ensure every human action has an agent equivalent.
The UI is a thin client over the API. The UI calls the same endpoints agents call. If a developer needs a new operation, they add an API endpoint, which is automatically a tool. There is no way to add a UI-only capability.
Every FastAPI route should declare four OpenAPI extensions: x-tool-name, x-tool-permission, x-tool-audit-category, and x-tool-entity-type. The tool_meta() helper provides consistent metadata. Full CI enforcement across all application routes is planned.
Quarterly manual audit:
Automated tests that replay common attorney workflows through the API (no browser). Each test:
If any step in an attorney workflow can't be replicated through API calls alone, the test fails. That is a parity violation.
Agents need current context, not just a snapshot from session start.
GET /agent/sessions/{id}/briefing: callable at any time, not just session start. Returns current case state:
# Case Briefing: {case_name}
Generated: {timestamp}
Attorney: {attorney_name}
Agent Role: {agent_type}
## Case Summary
{auto-generated 3-5 sentence case summary from evidence and facts}
## Key Entities
{list of people, organizations, and key entities with relationship counts}
## Open Issues
{claims and issues with linked fact counts and evidence counts}
## Recent Activity
{last 10 actions: evidence additions, fact approvals, report generations}
## Privilege Boundaries
- Agent owner: {attorney_name}
- Accessible cases: {case_list}
- Permitted operations: {operation_list}
- Restricted entities: {any entity-type restrictions}
GET /agent/sessions/{id}/changes?since={timestamp}: returns only what changed since the last check. Lightweight alternative to re-fetching the full briefing. Returns a list of change events with entity type, entity ID, change type, and timestamp.
For real-time updates, agents use the Event System: webhooks or polling. This is the preferred mechanism for agents that need to react to changes rather than periodically check for them.
All agent actions are logged with the same fidelity as human actions, plus agent-specific metadata.
The agent_audit_log table records every agent tool invocation with the acting agent, the tool called, the action performed, and the target entity. Scoped by firm_id and case_id like all other tables. The agent_owner_id is always populated; there is no anonymous agent action.
Implementation: See
backend/app/models/agent.pyfor the current schema.
reasoning_trace captures why the agent took the action, which is critical for attorney reviewReasoning trace capture: Agents submit reasoning traces via an
X-Agent-Reasoningrequest header on each API call. The header value is a brief (max 500 char) explanation of why the agent is performing this action. The middleware extracts and stores it in the audit log. If the header is absent,reasoning_traceis stored as NULL. Agent SDKs should populate this header automatically from the agent's chain-of-thought.
The data model for agent authentication, sessions, events, webhook delivery, and usage tracking.
agent_api_keys — SHA-256 hashed API keys with display prefix, scoped by firm and directing attorney. Keys carry JSONB scopes and a rate limit.agent_sessions — Scoped work contexts created by exchanging an API key. Each session carries a hashed bearer token, JSONB scopes, rate limit, and expiry.agent_audit_log — Per-invocation audit trail recording the agent, tool, action, and target entity. See Agent Audit Trail for guarantees.Implementation: See
backend/app/models/agent.pyfor the current schema. Management endpoints ship in M2+.
The
eventsandusage_operationstables are implemented. Thewebhook_deliveriestable is not yet implemented.
-- Event log (supports polling endpoint) — IMPLEMENTED
CREATE TABLE events (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
event_type VARCHAR NOT NULL, -- evidence.created, fact.created, etc.
entity_type VARCHAR,
entity_id UUID,
actor_type VARCHAR NOT NULL, -- human, agent, system
actor_id UUID,
data JSONB, -- event-specific payload
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for polling: (firm_id, case_id, created_at) + (event_type)
-- Retention: 90 days default, configurable per firm
-- Webhook delivery tracking — NOT YET IMPLEMENTED
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY,
session_id UUID NOT NULL REFERENCES agent_sessions(id),
event_id UUID NOT NULL REFERENCES events(id),
status VARCHAR DEFAULT 'pending', -- pending, delivered, failed, dead_letter
attempts INT DEFAULT 0,
last_attempt_at TIMESTAMPTZ,
next_retry_at TIMESTAMPTZ,
response_status INT, -- HTTP status from webhook endpoint
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- AI operation usage tracking — IMPLEMENTED
CREATE TABLE usage_operations (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
operation_type VARCHAR NOT NULL, -- research_query, case_brief, evidence_ingestion, etc.
operation_price DECIMAL(10,2) NOT NULL, -- rate card price charged
llm_cost DECIMAL(10,4), -- actual LLM cost (internal)
manual_equivalent DECIMAL(10,2), -- estimated manual cost
actor_type VARCHAR NOT NULL, -- human, agent
actor_id UUID,
agent_session_id UUID, -- NULL if human-initiated
job_id UUID, -- associated job, if async
client_billable BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Specialized agent roles work together under attorney direction. Each role has scoped permissions and a defined operational domain.
| Agent | Capabilities | Permission Scope | Phase |
|---|---|---|---|
| Intake Agent | Triage new uploads, extract metadata, classify, flag items requiring review | read + write on evidence, entities, relationships |
2 |
| Research Agent | Search evidence, find patterns, identify contradictions, traverse relationships | read + analyze on all entities |
2 |
| Drafting Agent | Generate reports from tools, compose analyses, produce court-ready output | read on all entities, write on reports |
2 |
| Client-Facing Agent | Power "Chat with My Case": answer client questions bounded by client permissions | read on client-visible evidence and facts only |
2 |
Agents do not communicate directly with each other. They operate independently on shared data:
The database is the coordination layer. Agents compose through shared state, not message passing. The attorney reviews each agent's output independently.
Agents are platform-hosted. Intactus runs agent logic server-side; attorneys do not deploy or manage agent code. The platform owns the execution environment, the API credentials, and the session lifecycle.
evidence.search, facts.create)In a future milestone, Intactus may open external agent access, allowing third-party agents to authenticate via API keys and call the same tool layer. This would use the existing API key and session model described in Agent Authentication & Sessions. Internal platform-hosted agents and external third-party agents would share the same permission system, audit trail, and cost controls.
Each practice-area module registers domain-specific API endpoints that agents discover automatically through the OpenAPI spec:
| Module | Example Endpoints |
|---|---|
| Family Law | /family-law/detect-custody-violations, /family-law/analyze-parenting-time, /family-law/flag-alienation-indicators |
| Employment | /employment/detect-hostile-environment, /employment/analyze-disparate-treatment, /employment/flag-retaliation-timeline |
| Criminal Defense | /criminal/identify-impeachment-material, /criminal/analyze-alibi-evidence, /criminal/detect-witness-inconsistencies |
| Defamation | /defamation/assess-publication-reach, /defamation/identify-false-statements, /defamation/calculate-damages-indicators |
Modules are installed per-firm. When installed, their endpoints appear in the OpenAPI spec and are immediately available to all agents working cases in that firm. No agent code changes required.
Standards that apply to every endpoint in the platform.
All .list endpoints use cursor-based pagination:
GET /cases/{id}/evidence?cursor={opaque_cursor}&limit=50
→ { items: [...], next_cursor: "abc123", has_more: true }
limit: 50. Max: 100.next_cursor is opaque; clients don't parse it. Pass it as cursor on the next request.All POST (create) endpoints accept an Idempotency-Key header:
POST /cases/{id}/evidence
Idempotency-Key: client-generated-uuid
/v1/cases/{id}/evidenceSunset header on responses.How agents get credentials, connect, and operate.
Attorneys generate API keys for agents through the dashboard. Each key is scoped:
| Scope | Description |
|---|---|
agent_owner_id |
The attorney who owns this key. All agent actions are attributed to this attorney. |
allowed_cases |
Which cases the agent can access (subset of the attorney's assigned cases). |
operation_permissions |
Permitted operation types: read, write, delete, analyze. |
rate_limits |
Per-key rate limits (requests/min, requests/hour). |
Agents authenticate with Authorization: Bearer <api_key>. The same middleware that validates human bearer tokens validates agent keys, with additional agent-specific permission checks (case scope, operation type, entity type).
Agent sessions provide scoped, time-limited work contexts:
POST /agent/sessions with agent type, target case(s), requested permissionssession_id + case briefing (see Observability)PATCH /agent/sessions/{id}.DELETE /agent/sessions/{id}, or automatic on TTL expiry.Implementation Status: Not yet implemented. Automation rules and standing instructions are planned for a future milestone.
Attorneys configure automation rules that authorize agents to act reactively. An automation rule is:
{
"rule_id": "uuid",
"agent_type": "intake",
"trigger": "event:evidence.created",
"case_ids": ["uuid", "..."], // or null for all cases
"action": "Start intake session, run triage on new evidence",
"enabled": true,
"created_by": "attorney_uuid"
}When a matching event fires, the platform creates an agent session on the attorney's behalf and starts the configured agent. The attorney can view, pause, and delete automation rules through the dashboard.
This resolves the tension between "no independent action" and reactive agents: the Intake Agent doesn't act independently. It acts on standing instructions the attorney explicitly configured. Every automated session traces back to a specific automation rule created by a specific attorney.
API keys are long-lived credentials; sessions are short-lived work contexts. An agent MUST create a session before calling any tool endpoint (except tools.list and tools.search). The session scopes the agent's access for a specific work unit:
POST /agent/sessions. All tool calls require a valid session.This separation enables: key rotation without disrupting active work, per-session case scoping (narrower than the key's allowed_cases), per-session webhook registration, and session-level usage tracking.
The agent role sits in the permission system alongside existing roles:
| Role | Sees | Can Do |
|---|---|---|
| Firm admin | All cases in firm | Manage attorneys, billing, firm settings |
| Attorney | Assigned cases | Full evidence management, AI analysis, case administration |
| Staff | Assigned cases | Evidence intake, organization, search (no AI analysis) |
| Agent | Cases assigned to owner attorney | API operations scoped by permission grants |
| Client | Own case only | Upload evidence, portal features, Chat with My Case, secure messaging |
| Guest | Specific case(s) only | Read-only access to attorney dashboard view |
Every agent operates within strict boundaries:
agent_owner_id: The attorney who authorized the agent. Every agent action is attributable to this attorney.read, write, delete, analyze. A Research Agent may have read + analyze; an Intake Agent may have read + write.The
analyzepermission is required for any operation that invokes an LLM beyond ingestion-time processing. Specifically:reports.generate,issues.suggest_structure,relationships.traverse(multi-hop only), all Deep Analysis operations (M11+), and Chat with My Case queries. Ingestion-time LLM calls (classify, summarize, extract_facts) requirewritepermission on evidence, notanalyze, because they are data enrichment, not analysis.
Client visibility boundary: Evidence items have a
client_visibleflag (default:truefor client-uploaded evidence,falsefor attorney-added work product). The Client-Facing Agent's entity type restriction limits it to records whereclient_visible = true. Attorneys toggle visibility per evidence item. Facts inherit visibility from their source evidence item unless overridden.
Under the Kovel doctrine and post-Heppner analysis, attorney-client privilege extends to agents (human or AI) acting at the attorney's direction. Intactus enforces this structurally:
agent_owner_id linking it to the directing attorneyThis is not a policy; it is a data model constraint.
Every user action in the platform maps to a specific API endpoint (tool). Organized by domain.
M1 = implemented and available now. Future = planned for a later milestone.
All .list endpoints accept cursor and limit query parameters per API Conventions. All .search endpoints accept the same pagination parameters in the request body.
| Tool | Description |
|---|---|
request_magic_link |
Request a magic link (always returns 200) |
verify_magic_link |
Verify magic link token, create session |
logout |
Revoke current session |
list_sessions |
List current user's active sessions |
revoke_session |
Revoke a specific session |
| Tool | Description |
|---|---|
firms.get |
Get the authenticated user's firm |
firms.update |
Update firm settings (firm_admin only) |
| Tool | Description |
|---|---|
users.me |
Get authenticated user's profile |
users.list |
List users in the firm (paginated) |
users.get |
Get a user by ID |
users.create |
Invite a new user (firm_admin only, sends magic link) |
users.update |
Update user profile |
users.deactivate |
Deactivate a user (firm_admin only, soft delete) |
| Tool | Description |
|---|---|
cases.create |
Create a new case |
cases.get |
Get case details with participants |
cases.list |
List cases (filtered by permission) |
cases.update |
Update case metadata |
cases.archive |
Archive a case |
cases.add_participant |
Add attorney, staff, or client to case |
cases.remove_participant |
Remove participant from case |
Cases cannot be deleted, only archived. Archived cases remain accessible for data retention compliance.
For exact endpoint URLs, see the OpenAPI spec at
/api/docs.
Future (M2+):
| Tool | Endpoint | Description |
|---|---|---|
cases.get_intake_email |
GET /cases/{id}/intake-email |
Get the case-specific intake email address |
cases.configure |
PATCH /cases/{id}/settings |
Update case settings (expiry, intake email, etc.) |
| Tool | Endpoint | Description |
|---|---|---|
evidence.list |
GET /cases/{id}/evidence |
List evidence items with filters |
evidence.get |
GET /evidence/{id} |
Get evidence item details |
evidence.create |
POST /cases/{id}/evidence |
Create evidence metadata record |
evidence.update |
PATCH /evidence/{id} |
Update evidence metadata |
evidence.delete |
DELETE /evidence/{id} |
Delete evidence item |
evidence.search |
POST /cases/{id}/evidence/search |
Full-text + structured search |
evidence.upload |
POST /cases/{id}/evidence/upload |
Initiate upload (returns presigned URL) |
evidence.confirm_upload |
POST /evidence/uploads/{id}/confirm |
Confirm upload complete, trigger ingestion |
evidence.download |
GET /evidence/{id}/download |
Get presigned download URL |
evidence.get_processing_status |
GET /evidence/{id}/processing-status |
Check ingestion pipeline status |
Additional implemented endpoints not listed above: enrichment (POST /evidence/{id}/enrich, POST /evidence/{id}/cancel-enrichment), re-extraction (POST /evidence/{id}/re-extract, POST /evidence/{id}/extract-entities, POST /evidence/{id}/summarize, POST /evidence/{id}/extract-facts), inline view (GET /evidence/{id}/view).
| Tool | Endpoint | Description |
|---|---|---|
facts.list |
GET /cases/{id}/facts |
List facts with filters (supports search via query params) |
facts.get |
GET /facts/{id} |
Get fact details including all fact_evidence rows (source snippets, offsets, is_primary flag) |
facts.create |
POST /cases/{id}/facts |
Create a fact with one or more evidence sources; each source creates a fact_evidence row |
facts.update |
PATCH /facts/{id} |
Update fact text or metadata |
facts.delete |
DELETE /facts/{id} |
Hard-delete a fact |
facts.bulk_update |
POST /cases/{id}/facts/batch-update |
Batch update fact status (approve, dismiss, or revert) |
facts.update_citation |
PATCH /facts/{id}/citation |
Update a fact's citation text |
facts.link_entity |
POST /facts/{id}/entities |
Link an entity to a fact |
facts.unlink_entity |
DELETE /facts/{id}/entities/{entity_id} |
Unlink an entity from a fact |
facts.add_temporal_mention |
POST /facts/{id}/temporal-mentions |
Add a temporal mention to a fact |
facts.delete_temporal_mention |
DELETE /facts/{id}/temporal-mentions/{mention_id} |
Remove a temporal mention from a fact |
facts.bulk_link_entities |
POST /cases/{id}/facts/batch-link-entity |
Batch link entities to facts |
facts.bulk_link_temporal |
POST /cases/{id}/facts/batch-link-temporal |
Batch link temporal mentions to facts |
parse_date |
POST /parse-date |
Parse a natural language date string into structured date components |
Facts can be hard-deleted via DELETE /facts/{id} or status-changed to dismissed via bulk update.
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
claims.create |
POST /cases/{id}/claims |
Create a claim |
claims.get |
GET /claims/{id} |
Get claim details |
claims.list |
GET /cases/{id}/claims |
List claims in a case |
claims.update |
PATCH /claims/{id} |
Update claim title/description |
claims.delete |
DELETE /claims/{id} |
Delete a claim |
claims.reorder |
PATCH /cases/{id}/claims/reorder |
Reorder claims |
issues.create |
POST /claims/{id}/issues |
Create an issue under a claim |
issues.get |
GET /issues/{id} |
Get issue details |
issues.list |
GET /claims/{id}/issues |
List issues in a claim |
issues.update |
PATCH /issues/{id} |
Update issue title/description |
issues.delete |
DELETE /issues/{id} |
Delete an issue |
issues.reorder |
PATCH /claims/{id}/issues/reorder |
Reorder issues within a claim |
issues.link_fact |
POST /issues/{id}/facts/{fact_id} |
Link a fact to an issue |
issues.unlink_fact |
DELETE /issues/{id}/facts/{fact_id} |
Unlink a fact from an issue |
issues.link_evidence |
POST /issues/{id}/evidence/{evidence_id} |
Link evidence to an issue |
issues.unlink_evidence |
DELETE /issues/{id}/evidence/{evidence_id} |
Unlink evidence from an issue |
issues.suggest_structure |
POST /cases/{id}/issues/suggest |
AI-suggest issue structure from facts |
| Tool | Endpoint | Description |
|---|---|---|
entities.create |
POST /cases/{id}/entities |
Manually create an entity |
entities.get |
GET /entities/{id} |
Get entity details |
entities.list |
GET /cases/{id}/entities |
List extracted entities (supports search via query params) |
entities.update |
PATCH /entities/{id} |
Update entity metadata |
entities.delete |
DELETE /entities/{id} |
Delete an entity |
entities.merge |
POST /entities/{id}/merge |
Merge duplicate entities |
entities.unmerge |
POST /entities/{id}/unmerge |
Unmerge a previously merged entity |
entities.summarize |
POST /entities/{id}/summarize |
Generate an AI summary for an entity |
entities.get_facts |
GET /entities/{id}/facts |
List facts linked to an entity |
entities.dedup_suggestions |
GET /cases/{id}/entities/dedup-suggestions |
Get dedup suggestions (simple) |
entities.llm_dedup_suggestions |
GET /cases/{id}/entities/llm-dedup-suggestions |
Get LLM-powered dedup suggestions |
entities.list_dedup_suggestions |
GET /cases/{id}/dedup-suggestions |
List dedup suggestions with filtering |
entities.dedup_count |
GET /cases/{id}/dedup-suggestions/count |
Count pending dedup suggestions |
entities.dedup_clusters |
GET /cases/{id}/dedup-suggestions/clusters |
Get dedup suggestion clusters |
entities.dedup_comparison |
GET /cases/{id}/dedup-suggestions/{suggestion_id}/comparison |
Get detailed comparison for a dedup suggestion |
entities.dedup_approve |
POST /cases/{id}/dedup-suggestions/{suggestion_id}/approve |
Approve a dedup suggestion (merge) |
entities.dedup_reject |
POST /cases/{id}/dedup-suggestions/{suggestion_id}/reject |
Reject a dedup suggestion |
entities.dedup_bulk |
POST /cases/{id}/dedup-suggestions/bulk |
Bulk approve/reject dedup suggestions |
entities.dedup_sweep |
POST /cases/{id}/dedup-sweep |
Trigger a dedup sweep for a case |
entities.auto_merges |
GET /cases/{id}/auto-merges |
List auto-merge history |
| Tool | Endpoint | Description |
|---|---|---|
relationships.create |
POST /cases/{id}/relationships |
Create a relationship |
relationships.get |
GET /relationships/{id} |
Get relationship details |
relationships.list |
GET /cases/{id}/relationships |
List relationships |
relationships.update |
PATCH /relationships/{id} |
Update a relationship |
relationships.delete |
DELETE /relationships/{id} |
Delete a relationship |
relationships.get_facts |
GET /relationships/{id}/facts |
List facts linked to a relationship |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
reports.generate |
POST /cases/{id}/reports |
Generate a report (async, returns job_id) |
reports.get |
GET /reports/{id} |
Get report metadata |
reports.list |
GET /cases/{id}/reports |
List reports for a case |
reports.get_status |
GET /reports/{id}/status |
Check generation status |
reports.download |
GET /reports/{id}/download |
Get presigned download URL |
reports.delete |
DELETE /reports/{id} |
Delete a report |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
exports.create |
POST /cases/{id}/exports |
Create an export (format + scope, async) |
exports.get_status |
GET /exports/{id}/status |
Check export status |
exports.download |
GET /exports/{id}/download |
Get presigned download URL |
exports.list |
GET /cases/{id}/exports |
List exports for a case |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
messages.create |
POST /cases/{id}/messages |
Send a message in a case |
messages.list |
GET /cases/{id}/messages |
List messages in a case |
messages.get |
GET /messages/{id} |
Get a message |
messages.get_thread |
GET /messages/{id}/thread |
Get full message thread |
messages.search |
POST /cases/{id}/messages/search |
Search messages by text, sender, date range |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
chat.query |
POST /cases/{id}/chat |
Submit a client question. Returns answer with evidence citations. Requires client or client_facing_agent role. |
chat.history |
GET /cases/{id}/chat |
Get chat history for a case (client-scoped) |
Note: chat.query is the tool the Client-Facing Agent calls. It performs question classification (factual vs. legal advice), agentic retrieval over client-visible evidence, response generation with citations, and guardrail enforcement as a single composite endpoint. This is an intentional exception to the "one operation per endpoint" principle: the chat UX requires a single round-trip for responsiveness.
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
notifications.create |
POST /notifications |
Create a notification |
notifications.list |
GET /notifications |
List notifications for current user |
notifications.mark_read |
POST /notifications/{id}/read |
Mark notification as read |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
monitoring.create |
POST /cases/{id}/monitors |
Create a monitoring job |
monitoring.list |
GET /cases/{id}/monitors |
List monitors for a case |
monitoring.get |
GET /monitors/{id} |
Get monitor details |
monitoring.pause |
POST /monitors/{id}/pause |
Pause a monitor |
monitoring.resume |
POST /monitors/{id}/resume |
Resume a paused monitor |
monitoring.delete |
DELETE /monitors/{id} |
Delete a monitor |
monitoring.get_results |
GET /monitors/{id}/results |
Get monitoring results |
monitoring.search |
POST /cases/{id}/monitors/search |
Search monitors by URL, status, type |
monitoring.update |
PATCH /monitors/{id} |
Update monitor URL, schedule, or configuration |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
designations.create |
POST /transcripts/{id}/designations |
Mark a page:line range for trial |
designations.list |
GET /transcripts/{id}/designations |
List designations for a transcript |
designations.import_opposing |
POST /transcripts/{id}/designations/import |
Import opposing counsel's designations |
designations.add_counter |
POST /designations/{id}/counter |
Add counter-designation |
designations.update_status |
PATCH /designations/{id} |
Update designation status |
designations.export |
POST /transcripts/{id}/designations/export |
Export designation report |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
transcripts.upload |
POST /cases/{id}/transcripts |
Upload deposition transcript (PDF/text) |
transcripts.get |
GET /transcripts/{id} |
Get transcript with page:line structure |
transcripts.list |
GET /cases/{id}/transcripts |
List transcripts in a case |
transcripts.search |
POST /cases/{id}/transcripts/search |
Full-text search across transcripts |
transcripts.get_citations |
GET /transcripts/{id}/citations |
Get Bluebook citations for marked passages |
transcripts.link_exhibit |
POST /transcripts/{id}/exhibits/{evidence_id} |
Link transcript exhibit reference to evidence |
transcripts.unlink_exhibit |
DELETE /transcripts/{id}/exhibits/{evidence_id} |
Unlink exhibit |
transcripts.update |
PATCH /transcripts/{id} |
Update transcript metadata |
transcripts.delete |
DELETE /transcripts/{id} |
Delete a transcript |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
filters.create |
POST /cases/{id}/filters |
Save a filter configuration |
filters.list |
GET /cases/{id}/filters |
List saved filters |
filters.get |
GET /filters/{id} |
Get filter details |
filters.apply |
POST /filters/{id}/apply |
Apply a saved filter. Returns { result_type: "evidence"|"facts"|"timeline", items: [...], total_count, next_cursor }. The result_type is determined by the filter's target view. |
filters.delete |
DELETE /filters/{id} |
Delete a saved filter |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
timeline.query |
POST /cases/{id}/timeline |
Query composite timeline (filters: date range, entity, source type, classification, issue). Returns { events: [{ evidence_id, date, source_type, entity_ids, classifications, summary }], total_count, date_range: { min, max } }. Events are evidence items projected into timeline format with denormalized metadata for rendering. |
| Tool | Endpoint | Description |
|---|---|---|
ingestion.extract_text |
POST /evidence/{id}/extract-text |
Extract text from evidence |
ingestion.extract_entities |
POST /evidence/{id}/extract-entities |
Extract entities from text |
ingestion.extract_relationships |
POST /evidence/{id}/extract-relationships |
Extract relationships |
ingestion.summarize |
POST /evidence/{id}/summarize |
Generate summary |
ingestion.extract_facts |
POST /evidence/{id}/extract-facts |
Extract facts with citations |
Note: Hashing and manifest recording happen automatically during the upload confirmation step — they are not separate endpoints. Classification is not yet implemented as a standalone endpoint.
| Tool | Endpoint | Description |
|---|---|---|
jobs.get_status |
GET /jobs/{id} |
Get job status and progress |
jobs.list |
GET /jobs |
List jobs (filterable by case, type, status) |
jobs.cancel |
POST /jobs/{id}/cancel |
Cancel an in-flight job |
jobs.retry |
POST /jobs/{id}/retry |
Retry a failed job |
jobs.get_result |
GET /jobs/{id}/result |
Get job output (or presigned download URL) |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
audit.list |
GET /cases/{id}/audit |
List audit entries for a case |
audit.get |
GET /audit/{id} |
Get audit entry details |
audit.search |
POST /cases/{id}/audit/search |
Search audit log |
audit.export |
POST /cases/{id}/audit/export |
Export audit trail |
| Tool | Endpoint | Description |
|---|---|---|
agents.create_session |
POST /agent/sessions |
Create an agent session |
agents.get_session |
GET /agent/sessions/{id} |
Get session details |
agents.terminate_session |
DELETE /agent/sessions/{id} |
Terminate a session |
agents.list_permissions |
GET /agent/permissions |
List current agent's permissions |
agents.get_briefing |
GET /agent/sessions/{id}/briefing |
Get/refresh case briefing |
Implementation Status: Not yet implemented. A basic GET /usage endpoint exists for internal tracking, but the full billing infrastructure described below is not built.
| Tool | Endpoint | Description |
|---|---|---|
usage.get_case_summary |
GET /cases/{id}/usage |
Per-case usage: operations breakdown, total cost, included budget consumed, overage, manual-equivalent savings |
usage.get_firm_summary |
GET /usage |
Firm-level: all cases aggregated, included budget remaining, overage, period totals |
usage.get_agent_summary |
GET /agent/sessions/{id}/usage |
Usage for a specific agent session |
usage.list_operations |
GET /cases/{id}/usage/operations |
Itemized list of AI operations on a case (filterable by date, type, agent) |
usage.tag_billable |
PATCH /usage/operations/{id} |
Tag an operation as client-billable or firm-absorbed |
usage.export_client_report |
POST /cases/{id}/usage/export |
Generate client-facing cost export (PDF/CSV) with professional labels. Async, returns job_id. |
| Tool | Endpoint | Description |
|---|---|---|
tools.list |
GET /openapi.json |
Full OpenAPI spec (the tool registry) |
tools.search |
GET /tools/search?q={query} |
Search tools by name/description. Convenience endpoint that searches the OpenAPI spec's x-tool-name and description fields. Not a separate index; the OpenAPI spec is the single source of truth. |
How agents react to changes in the platform. Two delivery mechanisms support different agent architectures.
Implementation Status: Not yet implemented. Only polling-based event consumption is available.
For agents that can receive HTTP callbacks:
PATCH /agent/sessions/{id})GET /agent/sessions/{id}/dead-letters)Every webhook delivery includes an X-Intactus-Signature header:
X-Intactus-Signature: sha256=<HMAC-SHA256 of payload using webhook secret>
The webhook secret is generated when the agent registers its webhook URL. Agents MUST verify this signature before processing events to prevent spoofed payloads.
For simpler agents that pull rather than receive:
GET /events?since={timestamp}&types={event_types}: returns all events since the given timestamp?types=evidence.created,fact.created?wait=30 holds the connection up to 30 seconds if no events are available, reducing chattinessEvents returned by the polling endpoint are filtered by the caller's access permissions. Agents only see events for cases in their session's
case_ids. Human users only see events for their assigned cases. There is no way to poll for cross-case or cross-firm events.
21 event types are currently implemented:
| Event | Trigger |
|---|---|
evidence.created |
New evidence item added to case |
evidence.updated |
Evidence item metadata updated |
evidence.deleted |
Evidence item deleted |
evidence.reviewed |
Evidence item marked as reviewed |
evidence.re_extracted |
Evidence item re-extracted |
evidence.viewed |
Evidence item viewed inline |
evidence.processed |
Ingestion/enrichment pipeline completed for an evidence item |
case.deleted |
Case deleted |
job.completed |
Background job completed successfully |
job.failed |
Background job failed |
entity.created |
New entity extracted or manually created |
entity.updated |
Entity metadata updated |
entity.deleted |
Entity deleted |
entity.merged |
Duplicate entities were merged |
entity.unmerged |
Previously merged entity was unmerged |
entity.auto_merged |
Entities automatically merged during enrichment |
relationship.created |
New relationship created |
relationship.updated |
Relationship updated |
relationship.deleted |
Relationship deleted |
fact.created |
New fact extracted or manually created |
fact.updated |
Fact text, status, or metadata updated |
fact.deleted |
Fact deleted |
{
"event_id": "uuid",
"event_type": "evidence.processed",
"case_id": "uuid",
"entity_type": "evidence",
"entity_id": "uuid",
"actor_type": "human|agent|system",
"actor_id": "uuid",
"timestamp": "ISO8601",
"data": { }
}The data field contains event-specific payload (e.g., for evidence.processed, it includes the processing results and any extracted metadata).
How agents handle long-running tasks. Any operation that takes more than 5 seconds returns a job_id instead of blocking.
Agent calls tool that triggers async work (e.g., reports.generate)
↓
API returns immediately:
{
"job_id": "uuid",
"status": "queued",
"poll_url": "/jobs/{id}"
}
↓
Agent polls job status OR receives webhook on completion
↓
On completion: jobs.get_result(job_id) returns the output
- For file outputs: returns a presigned download URL
- For data outputs: returns the data inline
queued → processing → completed
→ failed
cancelling → cancelled
Failed jobs include actionable information:
{
"job_id": "uuid",
"status": "failed",
"error": {
"code": "processing_error",
"message": "OCR failed on page 3 of document",
"retry_guidance": "Retry with lower resolution or skip page 3",
"partial_results": {
"pages_processed": 2,
"pages_failed": 1
}
}
}Agents can cancel in-flight jobs via POST /jobs/{id}/cancel. Jobs that have already completed cannot be cancelled.
How agents upload and download files. All file transfers use presigned S3 URLs; agents never send file contents through the API.
1. Agent calls evidence.upload(case_id, filename, content_type, size_bytes)
↓
2. API returns:
{
"upload_url": "https://s3.amazonaws.com/...",
"upload_id": "uuid",
"expires_in": 3600
}
↓
3. Agent PUTs file directly to S3 via presigned URL
↓
4. Agent calls evidence.confirm_upload(upload_id) to trigger ingestion
↓
5. API returns { "job_id": "uuid" } for pipeline tracking
1. Agent calls evidence.download(evidence_id) or exports.download(export_id)
↓
2. API returns:
{
"download_url": "https://s3.amazonaws.com/...",
"expires_in": 3600,
"content_type": "application/pdf",
"size_bytes": 1048576
}
↓
3. Agent GETs file from presigned URL
Implementation Status: Not yet implemented. Batch upload endpoints are planned. Currently, agents upload files one at a time using the single-file upload flow above.
Every tool returns errors in a consistent, machine-readable format, so agents can handle errors programmatically without parsing human-readable messages.
{
"error": {
"code": "FORBIDDEN",
"message": "Agent does not have 'write:facts' permission on case {case_id}",
"details": {
"required_permission": "write:facts",
"agent_permissions": ["read:evidence", "read:facts"],
"case_id": "uuid"
},
"retry_after": null,
"suggestion": "Request additional permissions from the directing attorney"
}
}Error codes are UPPERCASE strings. The following codes are in use or reserved:
| Code | HTTP Status | Meaning |
|---|---|---|
VALIDATION_ERROR |
422 | Request body or parameters failed validation |
NOT_FOUND |
404 | Entity does not exist or is not accessible |
FORBIDDEN |
403 | Caller lacks required permission or scope |
UNAUTHORIZED |
401 | Missing or invalid authentication token |
RATE_LIMITED |
429 | Rate limit exceeded (check retry_after) |
CONFLICT |
409 | Concurrent modification or duplicate conflict |
INTERNAL_ERROR |
500 | Unexpected server error (includes correlation_id in details) |
QUOTA_EXCEEDED |
429 | Monthly AI operations cost cap or session limit reached (M2+) |
IDEMPOTENCY_BODY_MISMATCH |
422 | Idempotency key reused with different request body |
IDEMPOTENCY_CONFLICT |
409 | Concurrent request with same idempotency key in progress |
Every error includes:
retry_after in seconds, or null if not retryable)Protection against runaway agents and cost overruns.
Configurable by the attorney when generating the key:
| Limit | Default | Configurable |
|---|---|---|
| Requests per minute | 100 | Yes |
| Requests per hour | 10,000 | Yes |
| Concurrent requests | 10 | Yes |
| Quota | Default | Configurable |
|---|---|---|
| Max concurrent agent sessions per case | 5 | Yes |
| Max evidence items per upload batch | 100 | Yes |
Agent operations consume the same per-operation rate card as human-initiated operations. There is no separate "agent tier." When an Intake Agent classifies evidence, it's billed the same $0.15 per item as when an attorney triggers classification. See business-model.md for the full rate card.
Attorney-configurable cost controls:
| Control | Description | Default |
|---|---|---|
| Per-case monthly cap | Hard spending limit. Operations fail with quota_exceeded when reached. |
None (attorney sets) |
| Per-case alert threshold | Soft warning notification at configurable amount. No interruption. | 80% of cap (if cap set) |
| Firm monthly cap | Overall AI budget ceiling across all cases. | None (attorney sets) |
| Per-operation approval | Optional: require attorney confirmation before expensive operations. | Off |
When a cap is reached:
quota_exceeded error (429) with details.cap_type, details.current_spend, details.cap_amountsuggestion: "Per-case monthly cap reached. Contact directing attorney to increase the cap."The Vault + Analysis subscription tier includes a monthly AI operations budget per attorney. Usage tracking reflects: operations consumed, included budget remaining, and overage amount. See business-model.md for tier details.
Implementation Status: Not yet implemented. The approval gate and awaiting_approval/denied job states are planned for a future milestone.
When enabled on a case, expensive operations (configurable threshold) would require attorney confirmation before proceeding.
If an agent hits 50 consecutive errors within a 5-minute window:
Every API response includes:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1708534800
How we ensure every human action has an agent equivalent.
The UI is a thin client over the API. The UI calls the same endpoints agents call. If a developer needs a new operation, they add an API endpoint, which is automatically a tool. There is no way to add a UI-only capability.
Every FastAPI route should declare four OpenAPI extensions: x-tool-name, x-tool-permission, x-tool-audit-category, and x-tool-entity-type. The tool_meta() helper provides consistent metadata. Full CI enforcement across all application routes is planned.
Quarterly manual audit:
Automated tests that replay common attorney workflows through the API (no browser). Each test:
If any step in an attorney workflow can't be replicated through API calls alone, the test fails. That is a parity violation.
Agents need current context, not just a snapshot from session start.
GET /agent/sessions/{id}/briefing: callable at any time, not just session start. Returns current case state:
# Case Briefing: {case_name}
Generated: {timestamp}
Attorney: {attorney_name}
Agent Role: {agent_type}
## Case Summary
{auto-generated 3-5 sentence case summary from evidence and facts}
## Key Entities
{list of people, organizations, and key entities with relationship counts}
## Open Issues
{claims and issues with linked fact counts and evidence counts}
## Recent Activity
{last 10 actions: evidence additions, fact approvals, report generations}
## Privilege Boundaries
- Agent owner: {attorney_name}
- Accessible cases: {case_list}
- Permitted operations: {operation_list}
- Restricted entities: {any entity-type restrictions}
GET /agent/sessions/{id}/changes?since={timestamp}: returns only what changed since the last check. Lightweight alternative to re-fetching the full briefing. Returns a list of change events with entity type, entity ID, change type, and timestamp.
For real-time updates, agents use the Event System: webhooks or polling. This is the preferred mechanism for agents that need to react to changes rather than periodically check for them.
All agent actions are logged with the same fidelity as human actions, plus agent-specific metadata.
The agent_audit_log table records every agent tool invocation with the acting agent, the tool called, the action performed, and the target entity. Scoped by firm_id and case_id like all other tables. The agent_owner_id is always populated; there is no anonymous agent action.
Implementation: See
backend/app/models/agent.pyfor the current schema.
reasoning_trace captures why the agent took the action, which is critical for attorney reviewReasoning trace capture: Agents submit reasoning traces via an
X-Agent-Reasoningrequest header on each API call. The header value is a brief (max 500 char) explanation of why the agent is performing this action. The middleware extracts and stores it in the audit log. If the header is absent,reasoning_traceis stored as NULL. Agent SDKs should populate this header automatically from the agent's chain-of-thought.
The data model for agent authentication, sessions, events, webhook delivery, and usage tracking.
agent_api_keys — SHA-256 hashed API keys with display prefix, scoped by firm and directing attorney. Keys carry JSONB scopes and a rate limit.agent_sessions — Scoped work contexts created by exchanging an API key. Each session carries a hashed bearer token, JSONB scopes, rate limit, and expiry.agent_audit_log — Per-invocation audit trail recording the agent, tool, action, and target entity. See Agent Audit Trail for guarantees.Implementation: See
backend/app/models/agent.pyfor the current schema. Management endpoints ship in M2+.
The
eventsandusage_operationstables are implemented. Thewebhook_deliveriestable is not yet implemented.
-- Event log (supports polling endpoint) — IMPLEMENTED
CREATE TABLE events (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
event_type VARCHAR NOT NULL, -- evidence.created, fact.created, etc.
entity_type VARCHAR,
entity_id UUID,
actor_type VARCHAR NOT NULL, -- human, agent, system
actor_id UUID,
data JSONB, -- event-specific payload
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for polling: (firm_id, case_id, created_at) + (event_type)
-- Retention: 90 days default, configurable per firm
-- Webhook delivery tracking — NOT YET IMPLEMENTED
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY,
session_id UUID NOT NULL REFERENCES agent_sessions(id),
event_id UUID NOT NULL REFERENCES events(id),
status VARCHAR DEFAULT 'pending', -- pending, delivered, failed, dead_letter
attempts INT DEFAULT 0,
last_attempt_at TIMESTAMPTZ,
next_retry_at TIMESTAMPTZ,
response_status INT, -- HTTP status from webhook endpoint
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- AI operation usage tracking — IMPLEMENTED
CREATE TABLE usage_operations (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
operation_type VARCHAR NOT NULL, -- research_query, case_brief, evidence_ingestion, etc.
operation_price DECIMAL(10,2) NOT NULL, -- rate card price charged
llm_cost DECIMAL(10,4), -- actual LLM cost (internal)
manual_equivalent DECIMAL(10,2), -- estimated manual cost
actor_type VARCHAR NOT NULL, -- human, agent
actor_id UUID,
agent_session_id UUID, -- NULL if human-initiated
job_id UUID, -- associated job, if async
client_billable BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Specialized agent roles work together under attorney direction. Each role has scoped permissions and a defined operational domain.
| Agent | Capabilities | Permission Scope | Phase |
|---|---|---|---|
| Intake Agent | Triage new uploads, extract metadata, classify, flag items requiring review | read + write on evidence, entities, relationships |
2 |
| Research Agent | Search evidence, find patterns, identify contradictions, traverse relationships | read + analyze on all entities |
2 |
| Drafting Agent | Generate reports from tools, compose analyses, produce court-ready output | read on all entities, write on reports |
2 |
| Client-Facing Agent | Power "Chat with My Case": answer client questions bounded by client permissions | read on client-visible evidence and facts only |
2 |
Agents do not communicate directly with each other. They operate independently on shared data:
The database is the coordination layer. Agents compose through shared state, not message passing. The attorney reviews each agent's output independently.
Agents are platform-hosted. Intactus runs agent logic server-side; attorneys do not deploy or manage agent code. The platform owns the execution environment, the API credentials, and the session lifecycle.
evidence.search, facts.create)In a future milestone, Intactus may open external agent access, allowing third-party agents to authenticate via API keys and call the same tool layer. This would use the existing API key and session model described in Agent Authentication & Sessions. Internal platform-hosted agents and external third-party agents would share the same permission system, audit trail, and cost controls.
Each practice-area module registers domain-specific API endpoints that agents discover automatically through the OpenAPI spec:
| Module | Example Endpoints |
|---|---|
| Family Law | /family-law/detect-custody-violations, /family-law/analyze-parenting-time, /family-law/flag-alienation-indicators |
| Employment | /employment/detect-hostile-environment, /employment/analyze-disparate-treatment, /employment/flag-retaliation-timeline |
| Criminal Defense | /criminal/identify-impeachment-material, /criminal/analyze-alibi-evidence, /criminal/detect-witness-inconsistencies |
| Defamation | /defamation/assess-publication-reach, /defamation/identify-false-statements, /defamation/calculate-damages-indicators |
Modules are installed per-firm. When installed, their endpoints appear in the OpenAPI spec and are immediately available to all agents working cases in that firm. No agent code changes required.
The agent role sits in the permission system alongside existing roles:
| Role | Sees | Can Do |
|---|---|---|
| Firm admin | All cases in firm | Manage attorneys, billing, firm settings |
| Attorney | Assigned cases | Full evidence management, AI analysis, case administration |
| Staff | Assigned cases | Evidence intake, organization, search (no AI analysis) |
| Agent | Cases assigned to owner attorney | API operations scoped by permission grants |
| Client | Own case only | Upload evidence, portal features, Chat with My Case, secure messaging |
| Guest | Specific case(s) only | Read-only access to attorney dashboard view |
Every agent operates within strict boundaries:
agent_owner_id: The attorney who authorized the agent. Every agent action is attributable to this attorney.read, write, delete, analyze. A Research Agent may have read + analyze; an Intake Agent may have read + write.The
analyzepermission is required for any operation that invokes an LLM beyond ingestion-time processing. Specifically:reports.generate,issues.suggest_structure,relationships.traverse(multi-hop only), all Deep Analysis operations (M11+), and Chat with My Case queries. Ingestion-time LLM calls (classify, summarize, extract_facts) requirewritepermission on evidence, notanalyze, because they are data enrichment, not analysis.
Client visibility boundary: Evidence items have a
client_visibleflag (default:truefor client-uploaded evidence,falsefor attorney-added work product). The Client-Facing Agent's entity type restriction limits it to records whereclient_visible = true. Attorneys toggle visibility per evidence item. Facts inherit visibility from their source evidence item unless overridden.
Under the Kovel doctrine and post-Heppner analysis, attorney-client privilege extends to agents (human or AI) acting at the attorney's direction. Intactus enforces this structurally:
agent_owner_id linking it to the directing attorneyThis is not a policy; it is a data model constraint.
Every user action in the platform maps to a specific API endpoint (tool). Organized by domain.
M1 = implemented and available now. Future = planned for a later milestone.
All .list endpoints accept cursor and limit query parameters per API Conventions. All .search endpoints accept the same pagination parameters in the request body.
| Tool | Description |
|---|---|
request_magic_link |
Request a magic link (always returns 200) |
verify_magic_link |
Verify magic link token, create session |
logout |
Revoke current session |
list_sessions |
List current user's active sessions |
revoke_session |
Revoke a specific session |
| Tool | Description |
|---|---|
firms.get |
Get the authenticated user's firm |
firms.update |
Update firm settings (firm_admin only) |
| Tool | Description |
|---|---|
users.me |
Get authenticated user's profile |
users.list |
List users in the firm (paginated) |
users.get |
Get a user by ID |
users.create |
Invite a new user (firm_admin only, sends magic link) |
users.update |
Update user profile |
users.deactivate |
Deactivate a user (firm_admin only, soft delete) |
| Tool | Description |
|---|---|
cases.create |
Create a new case |
cases.get |
Get case details with participants |
cases.list |
List cases (filtered by permission) |
cases.update |
Update case metadata |
cases.archive |
Archive a case |
cases.add_participant |
Add attorney, staff, or client to case |
cases.remove_participant |
Remove participant from case |
Cases cannot be deleted, only archived. Archived cases remain accessible for data retention compliance.
For exact endpoint URLs, see the OpenAPI spec at
/api/docs.
Future (M2+):
| Tool | Endpoint | Description |
|---|---|---|
cases.get_intake_email |
GET /cases/{id}/intake-email |
Get the case-specific intake email address |
cases.configure |
PATCH /cases/{id}/settings |
Update case settings (expiry, intake email, etc.) |
| Tool | Endpoint | Description |
|---|---|---|
evidence.list |
GET /cases/{id}/evidence |
List evidence items with filters |
evidence.get |
GET /evidence/{id} |
Get evidence item details |
evidence.create |
POST /cases/{id}/evidence |
Create evidence metadata record |
evidence.update |
PATCH /evidence/{id} |
Update evidence metadata |
evidence.delete |
DELETE /evidence/{id} |
Delete evidence item |
evidence.search |
POST /cases/{id}/evidence/search |
Full-text + structured search |
evidence.upload |
POST /cases/{id}/evidence/upload |
Initiate upload (returns presigned URL) |
evidence.confirm_upload |
POST /evidence/uploads/{id}/confirm |
Confirm upload complete, trigger ingestion |
evidence.download |
GET /evidence/{id}/download |
Get presigned download URL |
evidence.get_processing_status |
GET /evidence/{id}/processing-status |
Check ingestion pipeline status |
Additional implemented endpoints not listed above: enrichment (POST /evidence/{id}/enrich, POST /evidence/{id}/cancel-enrichment), re-extraction (POST /evidence/{id}/re-extract, POST /evidence/{id}/extract-entities, POST /evidence/{id}/summarize, POST /evidence/{id}/extract-facts), inline view (GET /evidence/{id}/view).
| Tool | Endpoint | Description |
|---|---|---|
facts.list |
GET /cases/{id}/facts |
List facts with filters (supports search via query params) |
facts.get |
GET /facts/{id} |
Get fact details including all fact_evidence rows (source snippets, offsets, is_primary flag) |
facts.create |
POST /cases/{id}/facts |
Create a fact with one or more evidence sources; each source creates a fact_evidence row |
facts.update |
PATCH /facts/{id} |
Update fact text or metadata |
facts.delete |
DELETE /facts/{id} |
Hard-delete a fact |
facts.bulk_update |
POST /cases/{id}/facts/batch-update |
Batch update fact status (approve, dismiss, or revert) |
facts.update_citation |
PATCH /facts/{id}/citation |
Update a fact's citation text |
facts.link_entity |
POST /facts/{id}/entities |
Link an entity to a fact |
facts.unlink_entity |
DELETE /facts/{id}/entities/{entity_id} |
Unlink an entity from a fact |
facts.add_temporal_mention |
POST /facts/{id}/temporal-mentions |
Add a temporal mention to a fact |
facts.delete_temporal_mention |
DELETE /facts/{id}/temporal-mentions/{mention_id} |
Remove a temporal mention from a fact |
facts.bulk_link_entities |
POST /cases/{id}/facts/batch-link-entity |
Batch link entities to facts |
facts.bulk_link_temporal |
POST /cases/{id}/facts/batch-link-temporal |
Batch link temporal mentions to facts |
parse_date |
POST /parse-date |
Parse a natural language date string into structured date components |
Facts can be hard-deleted via DELETE /facts/{id} or status-changed to dismissed via bulk update.
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
claims.create |
POST /cases/{id}/claims |
Create a claim |
claims.get |
GET /claims/{id} |
Get claim details |
claims.list |
GET /cases/{id}/claims |
List claims in a case |
claims.update |
PATCH /claims/{id} |
Update claim title/description |
claims.delete |
DELETE /claims/{id} |
Delete a claim |
claims.reorder |
PATCH /cases/{id}/claims/reorder |
Reorder claims |
issues.create |
POST /claims/{id}/issues |
Create an issue under a claim |
issues.get |
GET /issues/{id} |
Get issue details |
issues.list |
GET /claims/{id}/issues |
List issues in a claim |
issues.update |
PATCH /issues/{id} |
Update issue title/description |
issues.delete |
DELETE /issues/{id} |
Delete an issue |
issues.reorder |
PATCH /claims/{id}/issues/reorder |
Reorder issues within a claim |
issues.link_fact |
POST /issues/{id}/facts/{fact_id} |
Link a fact to an issue |
issues.unlink_fact |
DELETE /issues/{id}/facts/{fact_id} |
Unlink a fact from an issue |
issues.link_evidence |
POST /issues/{id}/evidence/{evidence_id} |
Link evidence to an issue |
issues.unlink_evidence |
DELETE /issues/{id}/evidence/{evidence_id} |
Unlink evidence from an issue |
issues.suggest_structure |
POST /cases/{id}/issues/suggest |
AI-suggest issue structure from facts |
| Tool | Endpoint | Description |
|---|---|---|
entities.create |
POST /cases/{id}/entities |
Manually create an entity |
entities.get |
GET /entities/{id} |
Get entity details |
entities.list |
GET /cases/{id}/entities |
List extracted entities (supports search via query params) |
entities.update |
PATCH /entities/{id} |
Update entity metadata |
entities.delete |
DELETE /entities/{id} |
Delete an entity |
entities.merge |
POST /entities/{id}/merge |
Merge duplicate entities |
entities.unmerge |
POST /entities/{id}/unmerge |
Unmerge a previously merged entity |
entities.summarize |
POST /entities/{id}/summarize |
Generate an AI summary for an entity |
entities.get_facts |
GET /entities/{id}/facts |
List facts linked to an entity |
entities.dedup_suggestions |
GET /cases/{id}/entities/dedup-suggestions |
Get dedup suggestions (simple) |
entities.llm_dedup_suggestions |
GET /cases/{id}/entities/llm-dedup-suggestions |
Get LLM-powered dedup suggestions |
entities.list_dedup_suggestions |
GET /cases/{id}/dedup-suggestions |
List dedup suggestions with filtering |
entities.dedup_count |
GET /cases/{id}/dedup-suggestions/count |
Count pending dedup suggestions |
entities.dedup_clusters |
GET /cases/{id}/dedup-suggestions/clusters |
Get dedup suggestion clusters |
entities.dedup_comparison |
GET /cases/{id}/dedup-suggestions/{suggestion_id}/comparison |
Get detailed comparison for a dedup suggestion |
entities.dedup_approve |
POST /cases/{id}/dedup-suggestions/{suggestion_id}/approve |
Approve a dedup suggestion (merge) |
entities.dedup_reject |
POST /cases/{id}/dedup-suggestions/{suggestion_id}/reject |
Reject a dedup suggestion |
entities.dedup_bulk |
POST /cases/{id}/dedup-suggestions/bulk |
Bulk approve/reject dedup suggestions |
entities.dedup_sweep |
POST /cases/{id}/dedup-sweep |
Trigger a dedup sweep for a case |
entities.auto_merges |
GET /cases/{id}/auto-merges |
List auto-merge history |
| Tool | Endpoint | Description |
|---|---|---|
relationships.create |
POST /cases/{id}/relationships |
Create a relationship |
relationships.get |
GET /relationships/{id} |
Get relationship details |
relationships.list |
GET /cases/{id}/relationships |
List relationships |
relationships.update |
PATCH /relationships/{id} |
Update a relationship |
relationships.delete |
DELETE /relationships/{id} |
Delete a relationship |
relationships.get_facts |
GET /relationships/{id}/facts |
List facts linked to a relationship |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
reports.generate |
POST /cases/{id}/reports |
Generate a report (async, returns job_id) |
reports.get |
GET /reports/{id} |
Get report metadata |
reports.list |
GET /cases/{id}/reports |
List reports for a case |
reports.get_status |
GET /reports/{id}/status |
Check generation status |
reports.download |
GET /reports/{id}/download |
Get presigned download URL |
reports.delete |
DELETE /reports/{id} |
Delete a report |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
exports.create |
POST /cases/{id}/exports |
Create an export (format + scope, async) |
exports.get_status |
GET /exports/{id}/status |
Check export status |
exports.download |
GET /exports/{id}/download |
Get presigned download URL |
exports.list |
GET /cases/{id}/exports |
List exports for a case |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
messages.create |
POST /cases/{id}/messages |
Send a message in a case |
messages.list |
GET /cases/{id}/messages |
List messages in a case |
messages.get |
GET /messages/{id} |
Get a message |
messages.get_thread |
GET /messages/{id}/thread |
Get full message thread |
messages.search |
POST /cases/{id}/messages/search |
Search messages by text, sender, date range |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
chat.query |
POST /cases/{id}/chat |
Submit a client question. Returns answer with evidence citations. Requires client or client_facing_agent role. |
chat.history |
GET /cases/{id}/chat |
Get chat history for a case (client-scoped) |
Note: chat.query is the tool the Client-Facing Agent calls. It performs question classification (factual vs. legal advice), agentic retrieval over client-visible evidence, response generation with citations, and guardrail enforcement as a single composite endpoint. This is an intentional exception to the "one operation per endpoint" principle: the chat UX requires a single round-trip for responsiveness.
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
notifications.create |
POST /notifications |
Create a notification |
notifications.list |
GET /notifications |
List notifications for current user |
notifications.mark_read |
POST /notifications/{id}/read |
Mark notification as read |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
monitoring.create |
POST /cases/{id}/monitors |
Create a monitoring job |
monitoring.list |
GET /cases/{id}/monitors |
List monitors for a case |
monitoring.get |
GET /monitors/{id} |
Get monitor details |
monitoring.pause |
POST /monitors/{id}/pause |
Pause a monitor |
monitoring.resume |
POST /monitors/{id}/resume |
Resume a paused monitor |
monitoring.delete |
DELETE /monitors/{id} |
Delete a monitor |
monitoring.get_results |
GET /monitors/{id}/results |
Get monitoring results |
monitoring.search |
POST /cases/{id}/monitors/search |
Search monitors by URL, status, type |
monitoring.update |
PATCH /monitors/{id} |
Update monitor URL, schedule, or configuration |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
designations.create |
POST /transcripts/{id}/designations |
Mark a page:line range for trial |
designations.list |
GET /transcripts/{id}/designations |
List designations for a transcript |
designations.import_opposing |
POST /transcripts/{id}/designations/import |
Import opposing counsel's designations |
designations.add_counter |
POST /designations/{id}/counter |
Add counter-designation |
designations.update_status |
PATCH /designations/{id} |
Update designation status |
designations.export |
POST /transcripts/{id}/designations/export |
Export designation report |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
transcripts.upload |
POST /cases/{id}/transcripts |
Upload deposition transcript (PDF/text) |
transcripts.get |
GET /transcripts/{id} |
Get transcript with page:line structure |
transcripts.list |
GET /cases/{id}/transcripts |
List transcripts in a case |
transcripts.search |
POST /cases/{id}/transcripts/search |
Full-text search across transcripts |
transcripts.get_citations |
GET /transcripts/{id}/citations |
Get Bluebook citations for marked passages |
transcripts.link_exhibit |
POST /transcripts/{id}/exhibits/{evidence_id} |
Link transcript exhibit reference to evidence |
transcripts.unlink_exhibit |
DELETE /transcripts/{id}/exhibits/{evidence_id} |
Unlink exhibit |
transcripts.update |
PATCH /transcripts/{id} |
Update transcript metadata |
transcripts.delete |
DELETE /transcripts/{id} |
Delete a transcript |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
filters.create |
POST /cases/{id}/filters |
Save a filter configuration |
filters.list |
GET /cases/{id}/filters |
List saved filters |
filters.get |
GET /filters/{id} |
Get filter details |
filters.apply |
POST /filters/{id}/apply |
Apply a saved filter. Returns { result_type: "evidence"|"facts"|"timeline", items: [...], total_count, next_cursor }. The result_type is determined by the filter's target view. |
filters.delete |
DELETE /filters/{id} |
Delete a saved filter |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
timeline.query |
POST /cases/{id}/timeline |
Query composite timeline (filters: date range, entity, source type, classification, issue). Returns { events: [{ evidence_id, date, source_type, entity_ids, classifications, summary }], total_count, date_range: { min, max } }. Events are evidence items projected into timeline format with denormalized metadata for rendering. |
| Tool | Endpoint | Description |
|---|---|---|
ingestion.extract_text |
POST /evidence/{id}/extract-text |
Extract text from evidence |
ingestion.extract_entities |
POST /evidence/{id}/extract-entities |
Extract entities from text |
ingestion.extract_relationships |
POST /evidence/{id}/extract-relationships |
Extract relationships |
ingestion.summarize |
POST /evidence/{id}/summarize |
Generate summary |
ingestion.extract_facts |
POST /evidence/{id}/extract-facts |
Extract facts with citations |
Note: Hashing and manifest recording happen automatically during the upload confirmation step — they are not separate endpoints. Classification is not yet implemented as a standalone endpoint.
| Tool | Endpoint | Description |
|---|---|---|
jobs.get_status |
GET /jobs/{id} |
Get job status and progress |
jobs.list |
GET /jobs |
List jobs (filterable by case, type, status) |
jobs.cancel |
POST /jobs/{id}/cancel |
Cancel an in-flight job |
jobs.retry |
POST /jobs/{id}/retry |
Retry a failed job |
jobs.get_result |
GET /jobs/{id}/result |
Get job output (or presigned download URL) |
Implementation Status: Not yet implemented.
| Tool | Endpoint | Description |
|---|---|---|
audit.list |
GET /cases/{id}/audit |
List audit entries for a case |
audit.get |
GET /audit/{id} |
Get audit entry details |
audit.search |
POST /cases/{id}/audit/search |
Search audit log |
audit.export |
POST /cases/{id}/audit/export |
Export audit trail |
| Tool | Endpoint | Description |
|---|---|---|
agents.create_session |
POST /agent/sessions |
Create an agent session |
agents.get_session |
GET /agent/sessions/{id} |
Get session details |
agents.terminate_session |
DELETE /agent/sessions/{id} |
Terminate a session |
agents.list_permissions |
GET /agent/permissions |
List current agent's permissions |
agents.get_briefing |
GET /agent/sessions/{id}/briefing |
Get/refresh case briefing |
Implementation Status: Not yet implemented. A basic GET /usage endpoint exists for internal tracking, but the full billing infrastructure described below is not built.
| Tool | Endpoint | Description |
|---|---|---|
usage.get_case_summary |
GET /cases/{id}/usage |
Per-case usage: operations breakdown, total cost, included budget consumed, overage, manual-equivalent savings |
usage.get_firm_summary |
GET /usage |
Firm-level: all cases aggregated, included budget remaining, overage, period totals |
usage.get_agent_summary |
GET /agent/sessions/{id}/usage |
Usage for a specific agent session |
usage.list_operations |
GET /cases/{id}/usage/operations |
Itemized list of AI operations on a case (filterable by date, type, agent) |
usage.tag_billable |
PATCH /usage/operations/{id} |
Tag an operation as client-billable or firm-absorbed |
usage.export_client_report |
POST /cases/{id}/usage/export |
Generate client-facing cost export (PDF/CSV) with professional labels. Async, returns job_id. |
| Tool | Endpoint | Description |
|---|---|---|
tools.list |
GET /openapi.json |
Full OpenAPI spec (the tool registry) |
tools.search |
GET /tools/search?q={query} |
Search tools by name/description. Convenience endpoint that searches the OpenAPI spec's x-tool-name and description fields. Not a separate index; the OpenAPI spec is the single source of truth. |
How agents react to changes in the platform. Two delivery mechanisms support different agent architectures.
Implementation Status: Not yet implemented. Only polling-based event consumption is available.
For agents that can receive HTTP callbacks:
PATCH /agent/sessions/{id})GET /agent/sessions/{id}/dead-letters)Every webhook delivery includes an X-Intactus-Signature header:
X-Intactus-Signature: sha256=<HMAC-SHA256 of payload using webhook secret>
The webhook secret is generated when the agent registers its webhook URL. Agents MUST verify this signature before processing events to prevent spoofed payloads.
For simpler agents that pull rather than receive:
GET /events?since={timestamp}&types={event_types}: returns all events since the given timestamp?types=evidence.created,fact.created?wait=30 holds the connection up to 30 seconds if no events are available, reducing chattinessEvents returned by the polling endpoint are filtered by the caller's access permissions. Agents only see events for cases in their session's
case_ids. Human users only see events for their assigned cases. There is no way to poll for cross-case or cross-firm events.
21 event types are currently implemented:
| Event | Trigger |
|---|---|
evidence.created |
New evidence item added to case |
evidence.updated |
Evidence item metadata updated |
evidence.deleted |
Evidence item deleted |
evidence.reviewed |
Evidence item marked as reviewed |
evidence.re_extracted |
Evidence item re-extracted |
evidence.viewed |
Evidence item viewed inline |
evidence.processed |
Ingestion/enrichment pipeline completed for an evidence item |
case.deleted |
Case deleted |
job.completed |
Background job completed successfully |
job.failed |
Background job failed |
entity.created |
New entity extracted or manually created |
entity.updated |
Entity metadata updated |
entity.deleted |
Entity deleted |
entity.merged |
Duplicate entities were merged |
entity.unmerged |
Previously merged entity was unmerged |
entity.auto_merged |
Entities automatically merged during enrichment |
relationship.created |
New relationship created |
relationship.updated |
Relationship updated |
relationship.deleted |
Relationship deleted |
fact.created |
New fact extracted or manually created |
fact.updated |
Fact text, status, or metadata updated |
fact.deleted |
Fact deleted |
{
"event_id": "uuid",
"event_type": "evidence.processed",
"case_id": "uuid",
"entity_type": "evidence",
"entity_id": "uuid",
"actor_type": "human|agent|system",
"actor_id": "uuid",
"timestamp": "ISO8601",
"data": { }
}The data field contains event-specific payload (e.g., for evidence.processed, it includes the processing results and any extracted metadata).
How agents handle long-running tasks. Any operation that takes more than 5 seconds returns a job_id instead of blocking.
Agent calls tool that triggers async work (e.g., reports.generate)
↓
API returns immediately:
{
"job_id": "uuid",
"status": "queued",
"poll_url": "/jobs/{id}"
}
↓
Agent polls job status OR receives webhook on completion
↓
On completion: jobs.get_result(job_id) returns the output
- For file outputs: returns a presigned download URL
- For data outputs: returns the data inline
queued → processing → completed
→ failed
cancelling → cancelled
Failed jobs include actionable information:
{
"job_id": "uuid",
"status": "failed",
"error": {
"code": "processing_error",
"message": "OCR failed on page 3 of document",
"retry_guidance": "Retry with lower resolution or skip page 3",
"partial_results": {
"pages_processed": 2,
"pages_failed": 1
}
}
}Agents can cancel in-flight jobs via POST /jobs/{id}/cancel. Jobs that have already completed cannot be cancelled.
How agents upload and download files. All file transfers use presigned S3 URLs; agents never send file contents through the API.
1. Agent calls evidence.upload(case_id, filename, content_type, size_bytes)
↓
2. API returns:
{
"upload_url": "https://s3.amazonaws.com/...",
"upload_id": "uuid",
"expires_in": 3600
}
↓
3. Agent PUTs file directly to S3 via presigned URL
↓
4. Agent calls evidence.confirm_upload(upload_id) to trigger ingestion
↓
5. API returns { "job_id": "uuid" } for pipeline tracking
1. Agent calls evidence.download(evidence_id) or exports.download(export_id)
↓
2. API returns:
{
"download_url": "https://s3.amazonaws.com/...",
"expires_in": 3600,
"content_type": "application/pdf",
"size_bytes": 1048576
}
↓
3. Agent GETs file from presigned URL
Implementation Status: Not yet implemented. Batch upload endpoints are planned. Currently, agents upload files one at a time using the single-file upload flow above.
Every tool returns errors in a consistent, machine-readable format, so agents can handle errors programmatically without parsing human-readable messages.
{
"error": {
"code": "FORBIDDEN",
"message": "Agent does not have 'write:facts' permission on case {case_id}",
"details": {
"required_permission": "write:facts",
"agent_permissions": ["read:evidence", "read:facts"],
"case_id": "uuid"
},
"retry_after": null,
"suggestion": "Request additional permissions from the directing attorney"
}
}Error codes are UPPERCASE strings. The following codes are in use or reserved:
| Code | HTTP Status | Meaning |
|---|---|---|
VALIDATION_ERROR |
422 | Request body or parameters failed validation |
NOT_FOUND |
404 | Entity does not exist or is not accessible |
FORBIDDEN |
403 | Caller lacks required permission or scope |
UNAUTHORIZED |
401 | Missing or invalid authentication token |
RATE_LIMITED |
429 | Rate limit exceeded (check retry_after) |
CONFLICT |
409 | Concurrent modification or duplicate conflict |
INTERNAL_ERROR |
500 | Unexpected server error (includes correlation_id in details) |
QUOTA_EXCEEDED |
429 | Monthly AI operations cost cap or session limit reached (M2+) |
IDEMPOTENCY_BODY_MISMATCH |
422 | Idempotency key reused with different request body |
IDEMPOTENCY_CONFLICT |
409 | Concurrent request with same idempotency key in progress |
Every error includes:
retry_after in seconds, or null if not retryable)Protection against runaway agents and cost overruns.
Configurable by the attorney when generating the key:
| Limit | Default | Configurable |
|---|---|---|
| Requests per minute | 100 | Yes |
| Requests per hour | 10,000 | Yes |
| Concurrent requests | 10 | Yes |
| Quota | Default | Configurable |
|---|---|---|
| Max concurrent agent sessions per case | 5 | Yes |
| Max evidence items per upload batch | 100 | Yes |
Agent operations consume the same per-operation rate card as human-initiated operations. There is no separate "agent tier." When an Intake Agent classifies evidence, it's billed the same $0.15 per item as when an attorney triggers classification. See business-model.md for the full rate card.
Attorney-configurable cost controls:
| Control | Description | Default |
|---|---|---|
| Per-case monthly cap | Hard spending limit. Operations fail with quota_exceeded when reached. |
None (attorney sets) |
| Per-case alert threshold | Soft warning notification at configurable amount. No interruption. | 80% of cap (if cap set) |
| Firm monthly cap | Overall AI budget ceiling across all cases. | None (attorney sets) |
| Per-operation approval | Optional: require attorney confirmation before expensive operations. | Off |
When a cap is reached:
quota_exceeded error (429) with details.cap_type, details.current_spend, details.cap_amountsuggestion: "Per-case monthly cap reached. Contact directing attorney to increase the cap."The Vault + Analysis subscription tier includes a monthly AI operations budget per attorney. Usage tracking reflects: operations consumed, included budget remaining, and overage amount. See business-model.md for tier details.
Implementation Status: Not yet implemented. The approval gate and awaiting_approval/denied job states are planned for a future milestone.
When enabled on a case, expensive operations (configurable threshold) would require attorney confirmation before proceeding.
If an agent hits 50 consecutive errors within a 5-minute window:
Every API response includes:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1708534800
How we ensure every human action has an agent equivalent.
The UI is a thin client over the API. The UI calls the same endpoints agents call. If a developer needs a new operation, they add an API endpoint, which is automatically a tool. There is no way to add a UI-only capability.
Every FastAPI route should declare four OpenAPI extensions: x-tool-name, x-tool-permission, x-tool-audit-category, and x-tool-entity-type. The tool_meta() helper provides consistent metadata. Full CI enforcement across all application routes is planned.
Quarterly manual audit:
Automated tests that replay common attorney workflows through the API (no browser). Each test:
If any step in an attorney workflow can't be replicated through API calls alone, the test fails. That is a parity violation.
Agents need current context, not just a snapshot from session start.
GET /agent/sessions/{id}/briefing: callable at any time, not just session start. Returns current case state:
# Case Briefing: {case_name}
Generated: {timestamp}
Attorney: {attorney_name}
Agent Role: {agent_type}
## Case Summary
{auto-generated 3-5 sentence case summary from evidence and facts}
## Key Entities
{list of people, organizations, and key entities with relationship counts}
## Open Issues
{claims and issues with linked fact counts and evidence counts}
## Recent Activity
{last 10 actions: evidence additions, fact approvals, report generations}
## Privilege Boundaries
- Agent owner: {attorney_name}
- Accessible cases: {case_list}
- Permitted operations: {operation_list}
- Restricted entities: {any entity-type restrictions}
GET /agent/sessions/{id}/changes?since={timestamp}: returns only what changed since the last check. Lightweight alternative to re-fetching the full briefing. Returns a list of change events with entity type, entity ID, change type, and timestamp.
For real-time updates, agents use the Event System: webhooks or polling. This is the preferred mechanism for agents that need to react to changes rather than periodically check for them.
All agent actions are logged with the same fidelity as human actions, plus agent-specific metadata.
The agent_audit_log table records every agent tool invocation with the acting agent, the tool called, the action performed, and the target entity. Scoped by firm_id and case_id like all other tables. The agent_owner_id is always populated; there is no anonymous agent action.
Implementation: See
backend/app/models/agent.pyfor the current schema.
reasoning_trace captures why the agent took the action, which is critical for attorney reviewReasoning trace capture: Agents submit reasoning traces via an
X-Agent-Reasoningrequest header on each API call. The header value is a brief (max 500 char) explanation of why the agent is performing this action. The middleware extracts and stores it in the audit log. If the header is absent,reasoning_traceis stored as NULL. Agent SDKs should populate this header automatically from the agent's chain-of-thought.
The data model for agent authentication, sessions, events, webhook delivery, and usage tracking.
agent_api_keys — SHA-256 hashed API keys with display prefix, scoped by firm and directing attorney. Keys carry JSONB scopes and a rate limit.agent_sessions — Scoped work contexts created by exchanging an API key. Each session carries a hashed bearer token, JSONB scopes, rate limit, and expiry.agent_audit_log — Per-invocation audit trail recording the agent, tool, action, and target entity. See Agent Audit Trail for guarantees.Implementation: See
backend/app/models/agent.pyfor the current schema. Management endpoints ship in M2+.
The
eventsandusage_operationstables are implemented. Thewebhook_deliveriestable is not yet implemented.
-- Event log (supports polling endpoint) — IMPLEMENTED
CREATE TABLE events (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
event_type VARCHAR NOT NULL, -- evidence.created, fact.created, etc.
entity_type VARCHAR,
entity_id UUID,
actor_type VARCHAR NOT NULL, -- human, agent, system
actor_id UUID,
data JSONB, -- event-specific payload
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for polling: (firm_id, case_id, created_at) + (event_type)
-- Retention: 90 days default, configurable per firm
-- Webhook delivery tracking — NOT YET IMPLEMENTED
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY,
session_id UUID NOT NULL REFERENCES agent_sessions(id),
event_id UUID NOT NULL REFERENCES events(id),
status VARCHAR DEFAULT 'pending', -- pending, delivered, failed, dead_letter
attempts INT DEFAULT 0,
last_attempt_at TIMESTAMPTZ,
next_retry_at TIMESTAMPTZ,
response_status INT, -- HTTP status from webhook endpoint
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- AI operation usage tracking — IMPLEMENTED
CREATE TABLE usage_operations (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
operation_type VARCHAR NOT NULL, -- research_query, case_brief, evidence_ingestion, etc.
operation_price DECIMAL(10,2) NOT NULL, -- rate card price charged
llm_cost DECIMAL(10,4), -- actual LLM cost (internal)
manual_equivalent DECIMAL(10,2), -- estimated manual cost
actor_type VARCHAR NOT NULL, -- human, agent
actor_id UUID,
agent_session_id UUID, -- NULL if human-initiated
job_id UUID, -- associated job, if async
client_billable BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Specialized agent roles work together under attorney direction. Each role has scoped permissions and a defined operational domain.
| Agent | Capabilities | Permission Scope | Phase |
|---|---|---|---|
| Intake Agent | Triage new uploads, extract metadata, classify, flag items requiring review | read + write on evidence, entities, relationships |
2 |
| Research Agent | Search evidence, find patterns, identify contradictions, traverse relationships | read + analyze on all entities |
2 |
| Drafting Agent | Generate reports from tools, compose analyses, produce court-ready output | read on all entities, write on reports |
2 |
| Client-Facing Agent | Power "Chat with My Case": answer client questions bounded by client permissions | read on client-visible evidence and facts only |
2 |
Agents do not communicate directly with each other. They operate independently on shared data:
The database is the coordination layer. Agents compose through shared state, not message passing. The attorney reviews each agent's output independently.
Agents are platform-hosted. Intactus runs agent logic server-side; attorneys do not deploy or manage agent code. The platform owns the execution environment, the API credentials, and the session lifecycle.
evidence.search, facts.create)In a future milestone, Intactus may open external agent access, allowing third-party agents to authenticate via API keys and call the same tool layer. This would use the existing API key and session model described in Agent Authentication & Sessions. Internal platform-hosted agents and external third-party agents would share the same permission system, audit trail, and cost controls.
Each practice-area module registers domain-specific API endpoints that agents discover automatically through the OpenAPI spec:
| Module | Example Endpoints |
|---|---|
| Family Law | /family-law/detect-custody-violations, /family-law/analyze-parenting-time, /family-law/flag-alienation-indicators |
| Employment | /employment/detect-hostile-environment, /employment/analyze-disparate-treatment, /employment/flag-retaliation-timeline |
| Criminal Defense | /criminal/identify-impeachment-material, /criminal/analyze-alibi-evidence, /criminal/detect-witness-inconsistencies |
| Defamation | /defamation/assess-publication-reach, /defamation/identify-false-statements, /defamation/calculate-damages-indicators |
Modules are installed per-firm. When installed, their endpoints appear in the OpenAPI spec and are immediately available to all agents working cases in that firm. No agent code changes required.
How we ensure every human action has an agent equivalent.
The UI is a thin client over the API. The UI calls the same endpoints agents call. If a developer needs a new operation, they add an API endpoint, which is automatically a tool. There is no way to add a UI-only capability.
Every FastAPI route should declare four OpenAPI extensions: x-tool-name, x-tool-permission, x-tool-audit-category, and x-tool-entity-type. The tool_meta() helper provides consistent metadata. Full CI enforcement across all application routes is planned.
Quarterly manual audit:
Automated tests that replay common attorney workflows through the API (no browser). Each test:
If any step in an attorney workflow can't be replicated through API calls alone, the test fails. That is a parity violation.
Agents need current context, not just a snapshot from session start.
GET /agent/sessions/{id}/briefing: callable at any time, not just session start. Returns current case state:
# Case Briefing: {case_name}
Generated: {timestamp}
Attorney: {attorney_name}
Agent Role: {agent_type}
## Case Summary
{auto-generated 3-5 sentence case summary from evidence and facts}
## Key Entities
{list of people, organizations, and key entities with relationship counts}
## Open Issues
{claims and issues with linked fact counts and evidence counts}
## Recent Activity
{last 10 actions: evidence additions, fact approvals, report generations}
## Privilege Boundaries
- Agent owner: {attorney_name}
- Accessible cases: {case_list}
- Permitted operations: {operation_list}
- Restricted entities: {any entity-type restrictions}
GET /agent/sessions/{id}/changes?since={timestamp}: returns only what changed since the last check. Lightweight alternative to re-fetching the full briefing. Returns a list of change events with entity type, entity ID, change type, and timestamp.
For real-time updates, agents use the Event System: webhooks or polling. This is the preferred mechanism for agents that need to react to changes rather than periodically check for them.
All agent actions are logged with the same fidelity as human actions, plus agent-specific metadata.
The agent_audit_log table records every agent tool invocation with the acting agent, the tool called, the action performed, and the target entity. Scoped by firm_id and case_id like all other tables. The agent_owner_id is always populated; there is no anonymous agent action.
Implementation: See
backend/app/models/agent.pyfor the current schema.
reasoning_trace captures why the agent took the action, which is critical for attorney reviewReasoning trace capture: Agents submit reasoning traces via an
X-Agent-Reasoningrequest header on each API call. The header value is a brief (max 500 char) explanation of why the agent is performing this action. The middleware extracts and stores it in the audit log. If the header is absent,reasoning_traceis stored as NULL. Agent SDKs should populate this header automatically from the agent's chain-of-thought.
The data model for agent authentication, sessions, events, webhook delivery, and usage tracking.
agent_api_keys — SHA-256 hashed API keys with display prefix, scoped by firm and directing attorney. Keys carry JSONB scopes and a rate limit.agent_sessions — Scoped work contexts created by exchanging an API key. Each session carries a hashed bearer token, JSONB scopes, rate limit, and expiry.agent_audit_log — Per-invocation audit trail recording the agent, tool, action, and target entity. See Agent Audit Trail for guarantees.Implementation: See
backend/app/models/agent.pyfor the current schema. Management endpoints ship in M2+.
The
eventsandusage_operationstables are implemented. Thewebhook_deliveriestable is not yet implemented.
-- Event log (supports polling endpoint) — IMPLEMENTED
CREATE TABLE events (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
event_type VARCHAR NOT NULL, -- evidence.created, fact.created, etc.
entity_type VARCHAR,
entity_id UUID,
actor_type VARCHAR NOT NULL, -- human, agent, system
actor_id UUID,
data JSONB, -- event-specific payload
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for polling: (firm_id, case_id, created_at) + (event_type)
-- Retention: 90 days default, configurable per firm
-- Webhook delivery tracking — NOT YET IMPLEMENTED
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY,
session_id UUID NOT NULL REFERENCES agent_sessions(id),
event_id UUID NOT NULL REFERENCES events(id),
status VARCHAR DEFAULT 'pending', -- pending, delivered, failed, dead_letter
attempts INT DEFAULT 0,
last_attempt_at TIMESTAMPTZ,
next_retry_at TIMESTAMPTZ,
response_status INT, -- HTTP status from webhook endpoint
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- AI operation usage tracking — IMPLEMENTED
CREATE TABLE usage_operations (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
operation_type VARCHAR NOT NULL, -- research_query, case_brief, evidence_ingestion, etc.
operation_price DECIMAL(10,2) NOT NULL, -- rate card price charged
llm_cost DECIMAL(10,4), -- actual LLM cost (internal)
manual_equivalent DECIMAL(10,2), -- estimated manual cost
actor_type VARCHAR NOT NULL, -- human, agent
actor_id UUID,
agent_session_id UUID, -- NULL if human-initiated
job_id UUID, -- associated job, if async
client_billable BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Specialized agent roles work together under attorney direction. Each role has scoped permissions and a defined operational domain.
| Agent | Capabilities | Permission Scope | Phase |
|---|---|---|---|
| Intake Agent | Triage new uploads, extract metadata, classify, flag items requiring review | read + write on evidence, entities, relationships |
2 |
| Research Agent | Search evidence, find patterns, identify contradictions, traverse relationships | read + analyze on all entities |
2 |
| Drafting Agent | Generate reports from tools, compose analyses, produce court-ready output | read on all entities, write on reports |
2 |
| Client-Facing Agent | Power "Chat with My Case": answer client questions bounded by client permissions | read on client-visible evidence and facts only |
2 |
Agents do not communicate directly with each other. They operate independently on shared data:
The database is the coordination layer. Agents compose through shared state, not message passing. The attorney reviews each agent's output independently.
Agents are platform-hosted. Intactus runs agent logic server-side; attorneys do not deploy or manage agent code. The platform owns the execution environment, the API credentials, and the session lifecycle.
evidence.search, facts.create)In a future milestone, Intactus may open external agent access, allowing third-party agents to authenticate via API keys and call the same tool layer. This would use the existing API key and session model described in Agent Authentication & Sessions. Internal platform-hosted agents and external third-party agents would share the same permission system, audit trail, and cost controls.
Each practice-area module registers domain-specific API endpoints that agents discover automatically through the OpenAPI spec:
| Module | Example Endpoints |
|---|---|
| Family Law | /family-law/detect-custody-violations, /family-law/analyze-parenting-time, /family-law/flag-alienation-indicators |
| Employment | /employment/detect-hostile-environment, /employment/analyze-disparate-treatment, /employment/flag-retaliation-timeline |
| Criminal Defense | /criminal/identify-impeachment-material, /criminal/analyze-alibi-evidence, /criminal/detect-witness-inconsistencies |
| Defamation | /defamation/assess-publication-reach, /defamation/identify-false-statements, /defamation/calculate-damages-indicators |
Modules are installed per-firm. When installed, their endpoints appear in the OpenAPI spec and are immediately available to all agents working cases in that firm. No agent code changes required.
All agent actions are logged with the same fidelity as human actions, plus agent-specific metadata.
The agent_audit_log table records every agent tool invocation with the acting agent, the tool called, the action performed, and the target entity. Scoped by firm_id and case_id like all other tables. The agent_owner_id is always populated; there is no anonymous agent action.
Implementation: See
backend/app/models/agent.pyfor the current schema.
reasoning_trace captures why the agent took the action, which is critical for attorney reviewReasoning trace capture: Agents submit reasoning traces via an
X-Agent-Reasoningrequest header on each API call. The header value is a brief (max 500 char) explanation of why the agent is performing this action. The middleware extracts and stores it in the audit log. If the header is absent,reasoning_traceis stored as NULL. Agent SDKs should populate this header automatically from the agent's chain-of-thought.
The data model for agent authentication, sessions, events, webhook delivery, and usage tracking.
agent_api_keys — SHA-256 hashed API keys with display prefix, scoped by firm and directing attorney. Keys carry JSONB scopes and a rate limit.agent_sessions — Scoped work contexts created by exchanging an API key. Each session carries a hashed bearer token, JSONB scopes, rate limit, and expiry.agent_audit_log — Per-invocation audit trail recording the agent, tool, action, and target entity. See Agent Audit Trail for guarantees.Implementation: See
backend/app/models/agent.pyfor the current schema. Management endpoints ship in M2+.
The
eventsandusage_operationstables are implemented. Thewebhook_deliveriestable is not yet implemented.
-- Event log (supports polling endpoint) — IMPLEMENTED
CREATE TABLE events (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
event_type VARCHAR NOT NULL, -- evidence.created, fact.created, etc.
entity_type VARCHAR,
entity_id UUID,
actor_type VARCHAR NOT NULL, -- human, agent, system
actor_id UUID,
data JSONB, -- event-specific payload
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for polling: (firm_id, case_id, created_at) + (event_type)
-- Retention: 90 days default, configurable per firm
-- Webhook delivery tracking — NOT YET IMPLEMENTED
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY,
session_id UUID NOT NULL REFERENCES agent_sessions(id),
event_id UUID NOT NULL REFERENCES events(id),
status VARCHAR DEFAULT 'pending', -- pending, delivered, failed, dead_letter
attempts INT DEFAULT 0,
last_attempt_at TIMESTAMPTZ,
next_retry_at TIMESTAMPTZ,
response_status INT, -- HTTP status from webhook endpoint
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- AI operation usage tracking — IMPLEMENTED
CREATE TABLE usage_operations (
id UUID PRIMARY KEY,
firm_id UUID NOT NULL,
case_id UUID NOT NULL,
operation_type VARCHAR NOT NULL, -- research_query, case_brief, evidence_ingestion, etc.
operation_price DECIMAL(10,2) NOT NULL, -- rate card price charged
llm_cost DECIMAL(10,4), -- actual LLM cost (internal)
manual_equivalent DECIMAL(10,2), -- estimated manual cost
actor_type VARCHAR NOT NULL, -- human, agent
actor_id UUID,
agent_session_id UUID, -- NULL if human-initiated
job_id UUID, -- associated job, if async
client_billable BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Specialized agent roles work together under attorney direction. Each role has scoped permissions and a defined operational domain.
| Agent | Capabilities | Permission Scope | Phase |
|---|---|---|---|
| Intake Agent | Triage new uploads, extract metadata, classify, flag items requiring review | read + write on evidence, entities, relationships |
2 |
| Research Agent | Search evidence, find patterns, identify contradictions, traverse relationships | read + analyze on all entities |
2 |
| Drafting Agent | Generate reports from tools, compose analyses, produce court-ready output | read on all entities, write on reports |
2 |
| Client-Facing Agent | Power "Chat with My Case": answer client questions bounded by client permissions | read on client-visible evidence and facts only |
2 |
Agents do not communicate directly with each other. They operate independently on shared data:
The database is the coordination layer. Agents compose through shared state, not message passing. The attorney reviews each agent's output independently.
Agents are platform-hosted. Intactus runs agent logic server-side; attorneys do not deploy or manage agent code. The platform owns the execution environment, the API credentials, and the session lifecycle.
evidence.search, facts.create)In a future milestone, Intactus may open external agent access, allowing third-party agents to authenticate via API keys and call the same tool layer. This would use the existing API key and session model described in Agent Authentication & Sessions. Internal platform-hosted agents and external third-party agents would share the same permission system, audit trail, and cost controls.
Each practice-area module registers domain-specific API endpoints that agents discover automatically through the OpenAPI spec:
| Module | Example Endpoints |
|---|---|
| Family Law | /family-law/detect-custody-violations, /family-law/analyze-parenting-time, /family-law/flag-alienation-indicators |
| Employment | /employment/detect-hostile-environment, /employment/analyze-disparate-treatment, /employment/flag-retaliation-timeline |
| Criminal Defense | /criminal/identify-impeachment-material, /criminal/analyze-alibi-evidence, /criminal/detect-witness-inconsistencies |
| Defamation | /defamation/assess-publication-reach, /defamation/identify-false-statements, /defamation/calculate-damages-indicators |
Modules are installed per-firm. When installed, their endpoints appear in the OpenAPI spec and are immediately available to all agents working cases in that firm. No agent code changes required.