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 & Path | Purpose | Status |
|---|---|---|
POST /v1/sources/csv/upload | Upload a CSV file; each row becomes one event | REAL |
POST /v1/sources/documents/metadata | Submit document metadata + pre-extracted text | REAL, NO OCR |
POST /v1/sources/events | Submit a single structured JSON event | REAL |
GET /v1/ingestion-runs/{run_id} | Check the status of an ingestion run | REAL |
GET /v1/raw-events/{raw_event_id} | Fetch a single raw event by ID | REAL |
Full detail: Ingestion.
Outbound APIs — memory coming out of KrisVen
Outbound| Method & Path | Purpose | Status |
|---|---|---|
POST /v1/governed/agent-context | Token-budgeted, policy-checked context pack for an agent | REAL |
POST /v1/governed/retrieve | Raw governed memory list through the 21-stage gateway | REAL |
POST /v1/governed/context-pack | Same retrieval, framed for LLM injection | REAL |
POST /v1/memory/search | Simpler keyword-style memory search | REAL |
GET / POST /v1/context | Older context-pack route (pre-dates the governed layer) | REAL |
| MCP server / GPT Action / Slack app | Agent-surface connectors on top of the routes above | PLANNED — 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.
| Route | Purpose | Status |
|---|---|---|
POST /v1/sources/csv/upload | Real file upload, one row per event | REAL |
POST /v1/sources/documents/metadata | Document metadata + pre-extracted text | REAL, SHALLOW |
POST /v1/sources/events | Single structured JSON event | REAL |
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"
}
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
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."
| Connector | Status | Notes |
|---|---|---|
CSVConnector | REAL | Genuinely parses CSV bytes |
DocumentMetadataConnector | SHALLOW | Requires pre-extracted text |
CreditDocumentIngestionConnector | SHALLOW | Requires pre-supplied extracted entities |
SalesforceMockConnector | MOCK | Reads canned fixtures |
DatabaseMockConnector | MOCK | 2 hardcoded synthetic events |
LOSMockConnector | DEAD CODE | Credit demo bypasses it entirely |
CoreBankingStub, CreditBureauStub, KYCStub | STUB | Self-labeled "mode": "stub", need credentials that don't exist |
OracleCDCConnector, SAPConnector | STUB | Every method raises NotImplementedError |
JSONEventConnector | UNUSED | Trivial 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.)
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
| Setting | Default | What it does |
|---|---|---|
MAX_UPLOAD_SIZE_BYTES | 26214400 (25 MB) | Files over this are rejected before parsing |
CLAMAV_ENABLED | false | Malware scanning is off by default |
CLAMAV_HOST / CLAMAV_PORT | localhost / 3310 | Where to find the clamd daemon |
CLAMAV_FAIL_CLOSED | true | Behavior 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.
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
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
- Structured-payload validation (e.g. a prompt-injection payload embedded in a CSV text field) — a separate concern, handled downstream by the memory compiler's governance tagging, not this pipeline.
- No virus-definition auto-update monitoring or alerting.
- No rate limiting or per-tenant upload quotas.
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.
| Endpoint | Use it when... |
|---|---|
POST /v1/governed/agent-context | Building an agent, want a ready-to-use token-budgeted context pack. Start here. |
POST /v1/governed/retrieve | Want the raw governed memory list to format/filter yourself. |
POST /v1/governed/context-pack | Same retrieval, framed explicitly for LLM injection. |
POST /v1/memory/search | Simpler 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"
}'
| Field | Required | Notes |
|---|---|---|
purpose | yes | 1–256 chars, logged for audit |
query | yes | 1–2000 chars. Never stored/returned in plaintext — only its SHA-256 hash appears anywhere. |
subject_refs | yes, min 1 | Which entities this context is about |
memory_types | no | Filter to specific types; omit for all |
max_tokens | no, default 4096 | Clamped server-side to [256, 8192] regardless of request |
min_trust_score | no, default 0.60 | Excludes memory below this trust score |
clearance_level | no, 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.
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.
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
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.