# TUVL Framework Agent Rules This repository uses the **TUVL framework**, a declarative ASGI router that loads YAML configurations at startup to generate FastAPI routes, PostgreSQL models, and AI agent workflows. As an AI agent working in this codebase, you must follow these rules strictly to ensure the generated applications are valid and production-ready. ## 1. Declarative First - Always prefer YAML definitions over writing Python code. Business logic should be expressed as `Workflow` documents with sequences of steps. - **Never invent fields, step kinds, or document kinds.** Rely only on the specified TUVL vocabulary. Every YAML document uses the spec-wrapped envelope (`kind:` + `metadata:` + `spec:`) — the flat root-level form is rejected. - Allowed Document Kinds: `ModelDefinition`, `Workflow`, `DataSource`, `EmbeddingRegistry`, `CollectionRegistry`, `FederationProvider`, `AgentModel`, `Artifact`, `ProjectConfig`, `TelemetryConfig`, `SystemConfig`. ## 2. Directory Structure Conventions TUVL recursively searches the project directory for YAML files and dispatches them by their `kind:`. Though location doesn't matter strictly, we use these conventions: - `models/`: Contains `ModelDefinition`, `EmbeddingRegistry`, `CollectionRegistry`. - `artifacts/`: Named, versioned assets — prose `.md` with YAML front-matter (types `prompt` | `steering` | `skill`) and structured `kind: Artifact` YAML (types `guardrail` | `hook` | `mcp`). Referenced anywhere via `artifact://name[@version]`; pin `@version` for production. - `workflows/`: Contains `Workflow` files. - `datasources/`: Contains `DataSource` configurations. - `llms/`: Contains `AgentModel` configurations. - `federation/`: Contains `FederationProvider` configurations. - `nodes/`: **Must** contain custom Python nodes. ## 3. Python Custom Nodes Rules When YAML isn't enough, you can create a custom `Functional` Python node. - **Crucial Rule:** You **MUST** put only one `@node()` decorator per file. - The Python file name **MUST** exactly match the runner name. E.g., `@node("score_resume")` must be in `nodes/score_resume.py`. - Do not group multiple nodes into one file. ## 4. Workflow Context Strictness - `Workflow` triggers receive HTTP request data into a shared `context: dict[str, Any]`. - **Database Allowlist:** Every model accessed inside a workflow (via `ModelOp` or custom nodes) must be explicitly listed in `spec.context.models`. Missing this causes a `PermissionError`. - **Reserved Keys:** Do not write to `_session`, `_db`, `_step`, `_response`, `_last_error`, `_last_error_type`, `_api_status_code`, `_context_model_versions`, `_schema_version`, `_instance_id`, `_user_id`. ## 5. Workflow Routing Requirements - Every step returns a signal (e.g., `default`, `error`, `true`, `false`). - You **MUST** define explicit mapping for every non-default signal the step might emit in the step's `routes:` map. - If a signal is emitted that is not in `routes:` (and is not `default`), a `RuntimeError` is raised. ## 5.1 Step Kinds (closed set, 8) Use only these `kind:` values in workflow steps — never invent others: `Functional`, `Agent`, `Router`, `APICall`, `MCP`, `ModelOp`, `Response`, `HumanInTheLoop`. `AutonomousAgent` no longer exists. - `Agent` is the one LLM step and **REQUIRES `mode: completion | autonomous` at the step level** (no default — the validator errors and the runtime raises). `mode: completion` is a single retried LLM call (fields `system` / `prompt`); `mode: autonomous` is a bounded tool-calling loop (fields `steering` / `tools` (REQUIRED) / `max_iterations` / `token_budget`) where the model picks tools (each `agent.tools[].ref` names another step in the workflow), observes results, and re-decides until it emits one of a declared `outcome.enum`. Each tool's description (REQUIRED) is sourced from the referenced step's top-level `description:`. - **Outcome contract (both modes):** `agent.outcome: {write, format: json|text, enum, map}`. `write` is the context key receiving the result (default `_result`). With `enum` declared the model must return an `"outcome"` field holding exactly one declared value — that is the route signal; map every enum value plus the applicable reserved exits (`error`, `parse_error` / `timeout` in completion, `max_iterations` / `budget_exceeded` / `aborted` in autonomous, `guardrail_violation` when guardrails attach) in `routes:`. - **Steering & skills:** `agent.steering` is the persistent instruction, ALWAYS injected; `agent.skills` are injected when relevant. Both (and `system` / `prompt`) take inline text or `artifact://` references. - **Guardrails & hooks:** `agent.guardrails: {input|output|tools: [artifact://…]}` attach `type: guardrail` artifacts (closed checks: `json_schema`, `regex_deny`, `max_chars`, `pii_mask`, `llm_judge`); a failing check routes the reserved `guardrail_violation` signal. Observe-only `type: hook` artifacts attach per step (`hooks:`) or workflow-wide (`spec.hooks:`) and never affect flow. - `MCP` steps declare `mcp.server: artifact://` pointing at a `type: mcp` artifact that owns the connection config (transport / url / headers / command / args / env) — inline transport blocks on the step are rejected. - `Router` supports a multi-way `match:` switch (`match: { field: user.country }`) for data-driven branching — keep deterministic routing logic here, never push it into an agent. ## 6. PostgreSQL & Multi-tenancy - Every `ModelDefinition` creates a Postgres table and auto-generates CRUD endpoints. - Do not emit `tenant_id` fields or RLS (Row Level Security) clauses. The project is single-tenant only. - PII fields must be marked with `secure: true`. ## 7. Developer Tooling & CLI - **Running locally:** Use `tuvl dev` (or `uv run tuvl dev`) to start the hot-reloading dev server on `http://localhost:8000`. - **Auto-Login:** Use `tuvl dev --auto-login` to automatically inject the security key and bypass the Insight developer portal security screen (useful for rapid automated testing). - **Validating:** Use `tuvl validate` to check every YAML config, node, and cross-reference without starting the server. - **Shipping to production:** Use `tuvl ship` to validate the project, generate a production `Dockerfile` and a Helm chart under `deploy/chart//`, and build the container image (`--no-build` to skip the build, `--push` to publish). ## 8. API Security - **Trigger default-deny:** In production, every workflow trigger route (REST, versioned run, gRPC) requires a valid bearer token by default, even when the workflow declares no `metadata.required_scope` / `required_group`. Anonymous endpoints require an explicit `spec.trigger.public: true`. **Never** combine `public: true` with `required_scope` / `required_group` — validation rejects it. Dev mode exempts unscoped workflows so quickstarts run without a token. - **CRUD scope + group gating:** Auto-generated CRUD routes are scope-gated by convention (`{model}:read` / `:write` / `:delete`) and can additionally be pinned to IAM groups via `spec.access.{read,write,delete}_groups` on the `ModelDefinition` — a caller needs the scope AND the group. Leaving a tier's groups undeclared cascades it down from the next-more-privileged tier (write falls back to read, delete falls back to write). - **CRUD kill switch:** The entire auto-generated `/models/*` CRUD surface can be disabled project-wide with `spec.api.expose_model_crud: false` in `.tuvl/system.yaml`, or the `TUVL_EXPOSE_MODEL_CRUD` env var. > For complete syntax and schema details, refer to `docs/tuvl-agentic-manual.md`.