text-to-sql-agent-langgraph

Governance

This document states, explicitly and in writing, who is responsible for this project’s data-handling and security decisions, how sensitive material is classified, how security-relevant changes are controlled, how deviations from a control are handled, and how often any of this actually gets looked at again. It exists because “the author thought about it carefully” (SECURITY.md’s own phrase for this project’s security posture) is easier to trust when the thinking is written down and dated, not just asserted.

This is a solo-maintainer project. Everything below is scoped to what’s proportionate at that scale — a documented discipline one person actually follows, not a simulation of an enterprise governance function. See COMPLIANCE.md for how this maps against named external frameworks, and RISK_REGISTER.md for the living list of what this governance process is tracking.

Ownership

Suraj Kumar is the sole maintainer and is solely responsible for this project’s data-handling and security decisions — what gets connected to, what’s classified as sensitive, what SQL is allowed to run, what rate and cost limits are set, and whether an exception to any of those is granted. There is no separate security team, review board, or second approver. That’s a real limitation, not a formality — it’s stated here so it’s never ambiguous who made a given call, and so anyone evaluating this project’s risk posture knows exactly what “reviewed” means in this context: reviewed by one person, not independently verified by another (see SECURITY.md’s “has not been through an independent security review”).

Data classification policy

Current status (as of 2026-09-01): implemented as an enforced control. config/sensitive_columns.yaml (loaded by config/sensitive_columns.py) exists and is wired into two enforcement points: db/value_sampling.py never samples a column classified “restricted” into the schema prompt regardless of cardinality, and agent/nodes.py::validate_sql_node rejects (retryable) generated SQL that directly selects a “restricted” column. The file ships empty — the mechanism exists, but no column has been reviewed and classified yet, so this has no effect on any real question until that happens. See tests/test_sensitive_columns.py and tests/test_nodes_security_wiring.py for the enforcement’s regression coverage.

The three tiers

Any column exposed to the LLM or rendered in the UI is intended to fall into exactly one of three tiers:

What exists today

config/sensitive_columns.yaml + config/sensitive_columns.py, mirroring config/table_descriptions.yaml’s own pattern exactly: hand-authored, read fresh on every call (no caching, so a hand-edit takes effect on the very next question, no rebuild step), deliberately incomplete until a human reviews and classifies each column that matters for the connected database. Two independent enforcement points read it:

config/table_descriptions.yaml still documents table/column meaning, not sensitivity — it remains a separate file with a separate purpose, not read as a classification source.

A restricted column’s values, once a query is allowed to select it, are not separately redacted in the results table — the block happens at query-generation time, not as output filtering. Nothing here is authorization-aware (there’s no per-user identity in this app’s single-user model to authorize against) — see “Restricted” tier’s own note below on what a future multi-user deployment would still need to add.

Extended to policy documents (2026-09-10, optional feature, off by default): the same three-tier philosophy — hand-authored classification, enforced fail-closed, not authorization-aware — now also covers uploaded policy PDFs, a different data shape (a whole document/chunk, not a (table, column) pair). rag/store.py’s SensitivityCategory implements three specific “restricted”-equivalent categories, reviewed and signed off on for this project: compensation & pay, disciplinary/HR case content, and legal/litigation. A chunk tagged with any of these is never summarized into an answer — see SECURITY.md’s multi-source section and CLAUDE.md’s “Document/policy agentic RAG” for the mechanism. Same caveat as the column-level “Restricted” tier: this blocks everyone equally, it is not per-user access control.

The policy tiers, as enforced

Change control for security-relevant changes

Any change to one of the following is deliberate and documented, not an incidental side effect of an unrelated commit:

Every such change gets a dated entry in security-changelog.md, separate from ordinary feature/commit history, so a reviewer (including future-me) can answer “when and why did the high-cost threshold change” without reconstructing it from git log across unrelated commits. A changelog entry should name: the date, what changed (old value → new value, or old rule → new rule), why, and whether it’s permanent or tied to an exception (see below).

This is a log, not a gate — nothing here technically blocks a change from shipping without an entry. The discipline is “I write the entry before I consider the change done,” the same way tests or lint are part of “done” per CONTRIBUTING.md, not a separate optional step.

Exception process

Sometimes a control needs to be knowingly, temporarily relaxed — raising a rate limit for a demo, temporarily reclassifying a restricted column for testing once classification exists, disabling cost estimation to debug an unrelated issue. That’s a legitimate need, not a violation, provided it’s explicit, logged, and time-boxed rather than a silent, permanent drift away from the documented control.

The process, appropriate for a single-maintainer project (a documented discipline, not automated enforcement):

  1. State it before making the change. Write down which control is being relaxed and why.
  2. Log it as an accepted exception in RISK_REGISTER.md’s “Accepted exceptions” section: what was relaxed, the reason, the expiry/review date, and the severity of leaving it relaxed past that date.
  3. Also log it in security-changelog.md if the exception involves one of the change-controlled items above (it usually will) — the changelog captures what changed, the risk register captures that it’s a temporary, tracked deviation and when it gets revisited. The two entries should cross-reference each other by date.
  4. Revert or re-review by the stated date. On the review date, either the control is restored, or the exception is explicitly renewed with a new date and a restated reason — it never just quietly persists past its own expiry unnoticed.

An exception with no expiry date, or one that’s been silently extended past its review date without a new entry, should be treated as a control that’s effectively been abandoned — surface it, don’t let it sit.

Review cadence

Proportionate to a solo-maintainer project actively being developed, not an enterprise SOC schedule:

Cross-references

This document is one of five that together describe this project’s risk posture — each links back to the others so a reader can start from any one of them: