Developer Documentation

Ingestion, Data Safety & Retrieval

How to get data into KrisVen, how uploaded files are checked before they become memory, and how to pull qualified, policy-checked context back out for LLM inference. Every endpoint and behavior described here reflects the actual code — where something is a stub, mock, or planned rather than working today, it's labeled as such.

Technical Architecture

End to end: how a file or event becomes governed memory, and how an agent pulls it back out. Inbound APIs (blue) accept data; the governance gate (amber) applies before storage is retrievable; outbound APIs (purple) are where agents call in for inference.

Note: retrieval ranking inside the Policy-Aware Retrieval Gateway currently falls back to keyword-overlap scoring rather than true vector cosine similarity, despite pgvector being provisioned — see the Retrieval section for detail.

Capabilities

What's actually implemented today, in plain terms:

Entity Resolution

12-stage pipeline: deterministic matching on known identifiers, fuzzy matching, graph clustering (union-find), producing one golden record per entity.

Memory Synthesis

9-stage governed compiler turns a resolved record into typed, structured memory with governance tags and provenance attached.

Trust Scoring

An 8-signal weighted engine scores every memory candidate; the score drives its lifecycle state and whether it needs human review.

Policy Engine

Allow / redact / deny decisions per memory, per role, per sensitivity classification, evaluated on every retrieval request.

Human Review & Lifecycle

A 14-state lifecycle gates new memory in PENDING_VALIDATION until approved; only ACTIVE memory reaches an agent.

Full Audit Trail

Every access decision, policy evaluation, and retrieval writes a structured, permanent audit event with actor, timestamp, and reason.

Multi-Tenant Isolation

tenant_id is enforced at every repository and API layer, resolved from the JWT rather than trusted from the request body.

File Safety

Size limits, content-type verification, and optional ClamAV malware scanning run before an uploaded file is ever parsed.

Hybrid Retrieval Ranking

Blends keyword overlap, relationship match, freshness, confidence, and source reliability. Vector/semantic scoring is currently a keyword-overlap fallback, not true embedding similarity.

Token-Budgeted Context Assembly

Assembles an LLM-ready context pack within a server-enforced token budget; the raw query is hashed, never stored or echoed in plaintext.

Inbound APIs — data going into KrisVen

Inbound
Method & PathPurposeStatus
POST /v1/sources/csv/uploadUpload a CSV file; each row becomes one eventREAL
POST /v1/sources/documents/metadataSubmit document metadata + pre-extracted textREAL, NO OCR
POST /v1/sources/eventsSubmit a single structured JSON eventREAL
GET /v1/ingestion-runs/{run_id}Check the status of an ingestion runREAL
GET /v1/raw-events/{raw_event_id}Fetch a single raw event by IDREAL

Full detail: Ingestion.

Outbound APIs — memory coming out of KrisVen

Outbound
Method & PathPurposeStatus
POST /v1/governed/agent-contextToken-budgeted, policy-checked context pack for an agentREAL
POST /v1/governed/retrieveRaw governed memory list through the 21-stage gatewayREAL
POST /v1/governed/context-packSame retrieval, framed for LLM injectionREAL
POST /v1/memory/searchSimpler keyword-style memory searchREAL
GET / POST /v1/contextOlder context-pack route (pre-dates the governed layer)REAL
MCP server / GPT Action / Slack appAgent-surface connectors on top of the routes abovePLANNED — not built

Full detail: Retrieval.

Ingestion Overview

Three ways to get data into KrisVen today. All require a Bearer token and are served under http://localhost:8000 in local dev.

RoutePurposeStatus
POST /v1/sources/csv/uploadReal file upload, one row per eventREAL
POST /v1/sources/documents/metadataDocument metadata + pre-extracted textREAL, SHALLOW
POST /v1/sources/eventsSingle structured JSON eventREAL

CSV File Upload REAL

The only endpoint that accepts a real file today. Each row becomes one ingestion event. The file runs through the file-safety pipeline (size limit, content-type check, optional malware scan) before parsing.

curl -X POST "http://localhost:8000/v1/sources/csv/upload?tenant_id=acme&source_system=manual_upload&source_object=accounts&id_column=account_id" \
  -H "Authorization: Bearer mock-admin-token" \
  -F "file=@accounts.csv"

Response:

{
  "run_id": "...",
  "tenant_id": "acme",
  "source_system": "manual_upload",
  "rows_received": 240,
  "rows_queued": 240,
  "status": "queued"
}
If a file fails a safety check You get a 422 with code: "FILE_SAFETY_ERROR" and a human-readable reason — the CSV is never parsed.

Check progress:

curl "http://localhost:8000/v1/ingestion-runs/{run_id}?tenant_id=acme" \
  -H "Authorization: Bearer mock-admin-token"

Document Metadata REAL, NO OCR

Read this before integrating documents KrisVen does not parse PDFs, scan images, or read email attachments itself. There is no OCR or text-extraction code in this repo today. Whatever you send as extracted_text_stub is what becomes memory — extracting that text from the original file is your application's job, or a future integration (AWS Textract / Azure Document Intelligence are the intended post-MVP path per the code comments).
curl -X POST "http://localhost:8000/v1/sources/documents/metadata" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer mock-admin-token" \
  -d '{
    "tenant_id": "acme",
    "document_id": "DOC-4471",
    "title": "Q3 Financial Statement — Acme Corp",
    "mime_type": "application/pdf",
    "size_bytes": 184320,
    "extracted_text_stub": "Q3 revenue was $12.4M, up 8% YoY...",
    "actor": {"type": "user", "id": "analyst_1"}
  }'

Single JSON Event REAL

For structured data you already have as JSON — a webhook payload, a row from your own database.

curl -X POST "http://localhost:8000/v1/sources/events" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer mock-admin-token" \
  -d '{
    "tenant_id": "acme",
    "source_system": "internal_crm",
    "source_object": "account",
    "source_record_id": "ACC-1001",
    "operation": "UPSERT",
    "payload": {"name": "Acme Corp", "industry": "Manufacturing"},
    "source_timestamp": "2026-07-14T10:00:00Z",
    "actor": {"type": "user", "id": "analyst_1"}
  }'

Connector Inventory

12 connector classes exist under krisven/connectors/. Only 3 do real work, and none has dedicated test coverage yet. The connectors folder's own base.py says it plainly: "MVP connectors are mocks/stubs. Real connectors extend these interfaces post-MVP."

ConnectorStatusNotes
CSVConnectorREALGenuinely parses CSV bytes
DocumentMetadataConnectorSHALLOWRequires pre-extracted text
CreditDocumentIngestionConnectorSHALLOWRequires pre-supplied extracted entities
SalesforceMockConnectorMOCKReads canned fixtures
DatabaseMockConnectorMOCK2 hardcoded synthetic events
LOSMockConnectorDEAD CODECredit demo bypasses it entirely
CoreBankingStub, CreditBureauStub, KYCStubSTUBSelf-labeled "mode": "stub", need credentials that don't exist
OracleCDCConnector, SAPConnectorSTUBEvery method raises NotImplementedError
JSONEventConnectorUNUSEDTrivial pass-through, called by nothing

Plan accordingly — if a demo or pilot needs Salesforce/SAP/Oracle/core-banking data today, it has to go through the CSV or JSON event route as an export, not a live connector.

Authentication

Every route needs Authorization: Bearer <token>. In local dev with MOCK_AUTH=true (the default), the token mock-admin-token is treated as a system/admin actor with access to every tenant. See the Mock Auth warning before exposing any of this outside local dev.

Data Safety: Keeping Unsafe Uploads Out of Memory

Any file a user uploads is untrusted until proven otherwise. krisven/security/file_safety.py implements three checks, run in order, cheapest first, stopping at the first failure:

uploaded bytes
      │
      ▼
1. validate_size        reject if over the configured byte limit
      │
      ▼
2. validate_mime_type    sniff actual content, reject if it doesn't match
      │                  what the route expects (catches a renamed .exe)
      ▼
3. scan_for_malware      ClamAV scan via clamd, if enabled
      │
      ▼
   parser (CSVConnector, etc.)
Before this existed Zero file-safety checks existed anywhere in the ingestion path — no size limit, no content-type verification, no malware scanning. The CSV route accepted an upload and handed it straight to the parser.

Wired into POST /v1/sources/csv/upload today. If a future route adds raw document/PDF upload, wire check_upload() in before the bytes reach a parser there too — the function is generic, not CSV-specific.

Configuration

SettingDefaultWhat it does
MAX_UPLOAD_SIZE_BYTES26214400 (25 MB)Files over this are rejected before parsing
CLAMAV_ENABLEDfalseMalware scanning is off by default
CLAMAV_HOST / CLAMAV_PORTlocalhost / 3310Where to find the clamd daemon
CLAMAV_FAIL_CLOSEDtrueBehavior when the daemon is unreachable

Why malware scanning defaults to off: ClamAV needs a running daemon with a several-hundred-MB virus database — a real infra dependency, not something to force on every dev machine. Size and content-type checks still run regardless of this setting.

fail_closed — the setting that matters most before production If scanning is enabled but the daemon can't be reached: CLAMAV_FAIL_CLOSED=true (default) rejects the upload — safer, since a scanner outage can't silently let unscanned files through. CLAMAV_FAIL_CLOSED=false lets it through with a logged warning — dev-only.

Running ClamAV Locally

Defined in docker-compose.yml under an opt-in profile (first boot downloads the virus database, which takes a few minutes):

docker compose --profile clamav up -d clamav

Then in your .env:

CLAMAV_ENABLED=true
CLAMAV_HOST=clamav
CLAMAV_PORT=3310

Production containers install libmagic1 via infra/docker/api.Dockerfile for content-type sniffing. On a bare dev machine without libmagic, validate_mime_type detects the missing library, logs a warning, and skips that one check rather than crashing — size limits and malware scanning still run.

Testing Safely with EICAR

Don't use a real virus to test this. Use the industry-standard EICAR test string — every antivirus engine, including ClamAV, is specifically designed to flag this exact harmless byte sequence as "malware" for testing purposes:

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Upload that with ClamAV enabled and you should get a 422 FILE_SAFETY_ERROR — malware detected (Eicar-Signature). The same string backs the automated test suite (tests/security/test_file_safety.py), so detection is covered without a real malicious sample.

A Related Gap: MOCK_AUTH

Worth knowing before exposing endpoints externally apps/api/dependencies.py has a mock_auth mode (default true) where, if a JWT fails to decode, the request is not rejected — it's given a synthetic mock actor derived from whatever string was in the Authorization header, with no signature verification. Fine for local dev. If MOCK_AUTH=true is ever left on in an environment reachable by external callers, anyone can claim to be any tenant. Confirm MOCK_AUTH=false and a real JWT issuer before exposing ingestion or retrieval endpoints outside a trusted network.

What This Does Not Cover

Retrieval: Getting Qualified Memory for LLM Inference

Every endpoint below is real, live, and covered by tests — not aspirational. A planned SDK is described at the end, clearly marked as not yet built.

EndpointUse it when...
POST /v1/governed/agent-contextBuilding an agent, want a ready-to-use token-budgeted context pack. Start here.
POST /v1/governed/retrieveWant the raw governed memory list to format/filter yourself.
POST /v1/governed/context-packSame retrieval, framed explicitly for LLM injection.
POST /v1/memory/searchSimpler keyword-style search.

All four route through the same PolicyAwareRetrievalGateway — a 21-stage pipeline enforcing tenant isolation, memory lifecycle state, sensitivity clearance, and trust thresholds. Only ACTIVE-lifecycle memory is ever retrievable.

POST /v1/governed/agent-context

curl -X POST "http://localhost:8000/v1/governed/agent-context" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer mock-admin-token" \
  -d '{
    "purpose": "credit_review",
    "query": "What prior decisions affect Acme Corp'"'"'s current loan application?",
    "subject_refs": [{"subject_type": "borrower", "subject_id": "BORR-99881"}],
    "memory_types": ["decision", "temporal", "semantic"],
    "max_tokens": 4000,
    "min_trust_score": 0.60,
    "clearance_level": "internal"
  }'
FieldRequiredNotes
purposeyes1–256 chars, logged for audit
queryyes1–2000 chars. Never stored/returned in plaintext — only its SHA-256 hash appears anywhere.
subject_refsyes, min 1Which entities this context is about
memory_typesnoFilter to specific types; omit for all
max_tokensno, default 4096Clamped server-side to [256, 8192] regardless of request
min_trust_scoreno, default 0.60Excludes memory below this trust score
clearance_levelno, default "internal"Used in the policy sensitivity check

tenant_id and agent_id are not request fields — resolved server-side from your JWT, so a caller can never claim to act as a different tenant by changing the request body.

Response:

{
  "assembly_id": "...",
  "tenant_id": "acme",
  "agent_id": "credit_agent",
  "purpose": "credit_review",
  "query_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "items": [
    {
      "memory_id": "...",
      "memory_type": "decision",
      "trust_score": 0.87,
      "content_structured": { "...": "..." },
      "subject_id": "BORR-99881",
      "sensitivity": "confidential",
      "estimated_tokens": 312,
      "is_truncated": false
    }
  ],
  "token_budget": 4000,
  "tokens_used": 1840,
  "truncated_count": 0,
  "warnings": [],
  "assembled_at": "2026-07-14T10:00:00Z",
  "policy_decision": "allow"
}

A policy denial returns 403 — no content leaks in the response.

Before you inject this into a prompt content_structured is untrusted data — apply the same prompt-injection precautions you would for any external text. Check truncated_count/warnings to know if the token budget cut anything.
Ranking caveat Ranking blends semantic/vector score, keyword overlap, relationship match, freshness, confidence, source reliability, and prior success. The vector/semantic signal currently falls back to keyword-overlap scoring rather than true embedding cosine similarity, despite pgvector being provisioned — "relevant" today leans more on keyword match and the other structured signals than deep semantic similarity.

POST /v1/governed/retrieve and /context-pack

Lower-level access to the same pipeline, for the memory list itself rather than a pre-assembled context pack.

curl -X POST "http://localhost:8000/v1/governed/retrieve" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer mock-admin-token" \
  -d '{
    "tenant_id": "acme",
    "actor": {"actor_id": "analyst_1", "tenant_id": "acme", "roles": ["credit_officer"]},
    "purpose": "credit_review",
    "query": "Acme Corp payment history",
    "subject_refs": [{"subject_type": "borrower", "subject_id": "BORR-99881"}],
    "max_results": 10,
    "minimum_confidence": 0.70
  }'

Unlike /agent-context, this takes tenant_id and actor explicitly rather than deriving purely from the JWT — useful for service-to-service calls. Response is a ContextPack: memories[], warnings[], policy_decision, total_candidates, filtered_count, and stages_run[] (which of the 21 pipeline stages actually ran).

/context-pack is the same pipeline framed for LLM injection — prefer /agent-context for new integrations since it also handles token budgeting.

Authentication

Same as ingestion — Authorization: Bearer <token>. See the Mock Auth warning before exposing these endpoints outside local dev.

Planned: KrisVenMemoryClient SDK PLANNED

Not yet implemented A Python SDK wrapping these HTTP calls is planned but does not exist in the codebase today — there is no sdk/ package. The sketch below shows the intended shape so agent developers can see where the API is headed, without mistaking it for something installable now.
# PLANNED — not yet implemented. Use the curl examples above today.
from krisven.sdk import KrisVenMemoryClient  # does not exist yet

client = KrisVenMemoryClient(tenant_id="acme", auth_token=token)

context = await client.retrieve_memory(
    purpose="credit_review",
    query="What prior decisions affect Acme Corp's current loan application?",
    subject_refs=[{"subject_type": "borrower", "subject_id": "BORR-99881"}],
    memory_types=["decision", "temporal", "semantic"],
    max_tokens=4000,
)

# Planned: closing the loop for the trust-score/conflict-detection feedback path
await client.submit_answer_audit(
    assembly_id=context.assembly_id,
    used_memory_ids=[i.memory_id for i in context.items],
)

If/when built, this would wrap /v1/governed/agent-context plus a new audit-submission endpoint that doesn't exist yet either. Until then, the curl/HTTP examples above are the actual integration path — including for agent-facing connectors like an MCP server or a Custom GPT Action, which would call these same endpoints directly.