# Architecture

See `LEGATE_AGENT_BUILD_PLAN.md` Section 3 for the canonical diagram. This file
captures decisions made during the build.

## Backend shape

- **Sync SQLAlchemy 2.x** for Phases 0–1. FastAPI runs sync path operations in a
  threadpool; this keeps the auth/RBAC layer simple and trivially testable. The
  agent core and connector `execute()` (Phase 2+) introduce async where it pays
  off (outbound LLM/tool I/O).
- **UUID primary keys** and `created_at` / `updated_at` on every table.
- **Tenant isolation** is enforced in the repository layer: every tenant-scoped
  query is filtered by the principal's active `workspace_id`. The active
  workspace is carried as a claim in the JWT access token.

## Auth

- JWT access (short TTL) + refresh (long TTL), HS256 signed with
  `LEGATE_SECRET_KEY`. Refresh rotates and is type-checked.
- API keys are an alternative principal: workspace-scoped, scope-gated, stored as
  SHA-256 hashes. Plaintext is shown exactly once at creation.

## Audit

Append-only, hash-chained per workspace: `hash = sha256(prev_hash + canonical(entry))`.
Tampering with any entry breaks the chain from that point forward.

## Agent Core (Phase 2)

- **LLM layer** (`legate/llm`): a provider-agnostic async `LLMClient` with
  OpenRouter and Ollama implementations, a scripted `FakeLLMClient` for tests,
  and a config-driven factory. Clients do plain chat completions; the structured
  action protocol lives in the runner.
- **Loop** (`legate/agent/runner.py`): plan-act-observe. Each turn the model
  returns `{thought, action, args}`; the runner validates args, runs the action
  through the Policy Engine, executes allowed tools, and feeds results back as
  **untrusted observations**. Malformed output is retried (bounded) then fails
  safe; step/token/wall-clock budgets are enforced from the agent's guardrails
  with workspace/settings fallbacks.
- **Tools** (`legate/agent/tooling.py`): a registry of `ToolSpec` + async
  handler. Built-ins are `kb.search` (stubbed until Phase 3) and read-only
  `http.request`. The Phase 4 connector framework registers through the same
  interface.
- **Execution + streaming**: `POST /agents/{id}/run` creates a `Run` and (in
  production) schedules it in the background; an in-process `RunEventBus` pushes
  `step`/`run.status` events to WebSocket subscribers, while persisted `Step`
  rows remain the source of truth for replay. Tests flip `AGENT_RUN_INLINE` for
  deterministic, synchronous runs.

## RAG knowledge layer (Phase 3)

- **Embeddings** (`legate/rag/embeddings`): swappable `Embedder` — `OpenAIEmbedder`
  for production and a deterministic, offline `HashingEmbedder` for dev/CI — behind
  a config-driven factory.
- **Vector store** (`legate/rag/vectorstore`): swappable `VectorStore` —
  `QdrantVectorStore` (production) and an in-process `InMemoryVectorStore`
  (dev/CI). Collections are created lazily and isolated per workspace+KB.
- **Ingestion** (`legate/rag/ingestion.py`): parse (TXT/MD native; PDF via pypdf,
  DOCX via python-docx) → chunk (overlapping) → embed → upsert, tracking document
  status (`pending → processing → ready | failed`). Re-ingestion clears prior
  chunks/vectors first, so it is idempotent and retryable. Runs via a Celery task
  in production; `KB_INGEST_INLINE` makes it synchronous for tests.
- **Storage** (`legate/rag/storage.py`): uploaded files go to object storage
  (local filesystem now; S3 in Phase 8).
- **Retrieval** (`legate/rag/retrieval.py`): embeds the query, searches the
  workspace's KB collections, and returns ranked passages with their source
  document. Wired into the agent loop through the `kb.search` tool, which degrades
  to an empty result when retrieval is not configured.

## Connectors + credential vault (Phase 4)

- **Connector framework** (`legate/connectors`): each connector conforms to a
  `Connector` protocol (`validate_config`, `list_tools`, `test_connection`,
  `execute`) and registers itself in the catalog — adding one is a new module, no
  core changes. Connector tools reuse the agent's `ToolSpec`/`ToolResult`/
  `ToolContext`, so they plug straight into the loop. v1: `http`, `slack`, `email`.
- **Credential vault** (`legate/vault`): envelope encryption. A fresh per-record
  data key (DEK) encrypts the secret; the DEK is wrapped by a master key (KEK)
  from `LEGATE_MASTER_KEY`. Records carry a `key_version` so KEKs can be rotated
  (`Vault.rewrap`) without touching ciphertext. Plaintext exists only in memory
  during encrypt/decrypt and is never logged or returned by the API.
- **Tool resolution** (`legate/connectors/resolution.py`): each run builds a
  registry of built-in tools plus the tools of every connector installed in the
  workspace. A connector tool's handler decrypts its credential only when the
  tool is actually invoked, then calls `connector.execute(...)`.
- **Injection seams**: connectors get their httpx client from
  `legate.connectors.runtime`, and the Email connector isolates SMTP/IMAP behind
  small methods — so tests exercise real connector code with mocked transports.

## Policy Engine + approvals (Phase 5)

- **Policy Engine** (`legate/policy/engine.py`): every tool call is evaluated to
  `allow` / `deny` / `require_approval`. Non-allow-listed tools are denied;
  sensitive tools consult per-workspace rules (`workspace.settings_json['approval']`)
  which can `deny`, `auto_approve`, or (default) require human approval. Each
  sensitive-tool decision is written to the audit log.
- **Suspend/resume** (`legate/agent/runner.py`): on `require_approval` the runner
  records an `Approval` capturing the exact tool, arguments, and a snapshot of the
  transcript, then sets the run to `waiting_approval` and stops. `AgentRunner.resume`
  rebuilds the transcript from that snapshot; on approve it executes the action and
  continues the loop, on reject it feeds a rejection observation back so the agent
  replans. Resume runs inline (tests) or in the background (production), same as a
  fresh run.
- **Inbox** (`legate/api/approvals.py`): approvers (holders of `approvals:decide`)
  list and decide pending approvals. Approvals carry an expiry (`APPROVAL_TTL_HOURS`)
  and an optional role gate; expired or already-decided approvals are rejected with
  `409`. Every decision is audited.

## Workflow engine (Phase 6)

- **Definition**: a workflow is a graph (`definition_json`) of nodes and edges.
  Node types: `trigger`, `agent`, `tool`, `condition`, `transform`, `approval`,
  `delay` (`subworkflow` is reserved). Edges may carry a `when` ("true"/"false")
  branch off `condition`/`approval`/`tool` nodes.
- **Templating** (`legate/workflow/templating.py`): sandboxed `{{ path }}` over
  prior node outputs. Interpolation preserves type for a lone reference; condition
  expressions are evaluated by a hand-written AST walker that permits only
  literals, comparisons, boolean/`not`, and membership — no calls, attributes,
  subscripts, or arbitrary names.
- **Executor** (`legate/workflow/executor.py`): runs nodes in topological order,
  taking edges by branch value, persisting each node as a `Step`. `agent` nodes run
  a nested agent run; `tool` nodes go through the policy engine. `approval` nodes
  and sensitive `tool` nodes **pause** the run (creating an `Approval`); the same
  approvals inbox resumes it. Failed nodes retry with backoff per their node policy.
  Resume state (context + completed nodes) lives in `run.state_json`.
- **Triggers** (`legate/workflow/service.py`, `webhooks.py`): **manual**,
  **webhook** (per-workflow token + HMAC secret; `verify_signature` uses a constant-
  time compare), **schedule** (cron via `croniter`, driven by a Celery-beat task),
  and internal **events** (`emit_event`).
