SQL Policy¶
SQL policy is an extension framework for enforcing request-scoped SQL read controls. It is useful when an API request should only access rows or tables within the caller's business scope, such as a tenant, market, region, or store list.
The open-source agent provides the framework boundary and runtime hook points. It does not ship a concrete policy engine. Deployments that need enforcement should provide their own plugin package.
Framework Boundary¶
The open-source agent owns these responsibilities:
- Loading
agent.sql_policyfrom agent configuration. - Loading the configured plugin class from
agent.sql_policy.provider. - Passing the full raw
agent.sql_policymapping to the plugin. - Parsing request principal fields into
AppContext.principal. - Running a generic API pre-check for required
principal.*values. - Calling the plugin before read queries reach the database.
- Revalidating SQL after a plugin rewrite.
- Returning policy denial reasons to the tool/model layer.
The plugin owns these responsibilities:
- Defining the policy schema under
agent.sql_policy. - Validating plugin-specific policy fields.
- Matching policies to datasources, tables, columns, or other business concepts.
- Resolving values from the request principal.
- Rewriting read SQL or denying the query.
- Returning clear denial reasons that tell the model or caller what is missing.
Runtime Flow¶
When SQL policy is enabled, request handling follows this flow:
- The API auth provider creates an
AppContext. - Request-scoped attributes are stored on
AppContext.principal. - Agent configuration is loaded, including
agent.sql_policy. - If the raw policy config references
value_from: principal.<path>, the chat API checks that each referenced principal path is present before the agent starts. - The agent runs normally until a database read tool is called.
DBFuncTool.read_queryvalidates that the original SQL is read-only.- The configured plugin is loaded and called through
enforce_read(...). - If the plugin denies the query, the tool returns the plugin's reason and does not execute SQL.
- If the plugin returns rewritten SQL, the agent revalidates that rewritten SQL is still read-only.
- The validated SQL is executed against the target datasource.
The pre-check in step 4 is intentionally generic. It scans the raw policy mapping for value_from strings that start with principal.. It does not assume a specific policy type, column name, or business field.
Configuration Contract¶
The open-source agent only interprets these fields:
enabled turns on the framework. provider must be a Python class path in module:Class format.
All other fields under agent.sql_policy are passed through unchanged as SqlPolicyConfig.raw. Your plugin can define any schema it needs:
agent:
sql_policy:
enabled: true
provider: my_company.sql_policies:SqlPolicyProvider
policies:
- name: tenant_scope
type: row_filter
applies_to:
datasources: ["warehouse"]
tables: ["orders"]
condition:
column: tenant_id
operator: eq
value_from: principal.tenant.id
enforcement:
on_read: filter
on_unhandled: deny
In this example, the open-source agent uses enabled, provider, and the principal.tenant.id reference for pre-checking. The plugin decides what policies, type, applies_to, condition, and enforcement mean.
Plugin Interface¶
A plugin is a normal Python package installed in the same environment as datus-api. The provider class must accept a SqlPolicyConfig argument and implement enforce_read(...).
from typing import Any, Dict, Optional
from datus.tools.sql_policy import SqlPolicyConfig, EnforcementResult
class SqlPolicyProvider:
def __init__(self, config: Optional[SqlPolicyConfig] = None) -> None:
self.config = config or SqlPolicyConfig()
self.policies = self.config.raw.get("policies", []) or []
self._validate_policy_config()
def enforce_read(
self,
sql: str,
*,
datasource: str,
dialect: str,
principal: Optional[Dict[str, Any]],
) -> EnforcementResult:
principal = principal or {}
# Implement policy selection and enforcement here:
# - parse SQL and identify referenced tables
# - match policies for the datasource and tables
# - resolve configured values from principal
# - return a rewritten read query or deny with a clear reason
return EnforcementResult(allowed=True, sql=sql)
def _validate_policy_config(self) -> None:
# Raise an exception if required plugin-specific config is invalid.
pass
The result controls what happens next:
return EnforcementResult(
allowed=False,
reason="Missing required principal path: principal.tenant.id",
)
Use a SQL parser or database-safe query builder when rewriting SQL. Avoid string concatenation for policy predicates.
Request Principal¶
The principal is the request-scoped input that policy plugins use for caller attributes. For the default API auth provider, principal fields are read from the X-Datus-Principal header as a JSON object:
The plugin receives the parsed object:
Policy config can reference nested fields with principal.<path>:
The API pre-check treats missing keys, null, empty strings, and empty arrays as missing values.
X-Datus-User-Id is separate from the SQL policy principal. It identifies a caller for session isolation and is not copied into AppContext.principal.
Provider Contract¶
| Item | Contract |
|---|---|
agent.sql_policy.enabled |
Enables the SQL policy framework. |
agent.sql_policy.provider |
Python class path in module:Class format. Required when enabled. |
SqlPolicyConfig.raw |
Full raw agent.sql_policy mapping passed to the plugin. |
enforce_read(sql, datasource, dialect, principal) |
Called before read SQL is executed. |
EnforcementResult.allowed=True |
The query may continue. sql can contain the original or rewritten SQL. |
EnforcementResult.allowed=False |
The query is denied. reason is returned to the tool/model layer. |
applied_policies |
Optional policy names for logging and diagnostics. |
value_from: principal.* |
Optional convention used by the API pre-check to detect missing principal fields. |
Error Behavior¶
If SQL policy is enabled but no provider is configured, enforcement fails before SQL is executed.
If the configured provider class cannot be imported, initialized, or does not implement a callable enforce_read, enforcement fails with a SQL policy provider error.
If policy config references a missing principal.* value, the chat API fails before the agent starts:
The error message includes the missing principal path, for example principal.tenant.id.
If the plugin denies a query, SQL is not executed and the tool returns the plugin's reason.
If the plugin rewrites SQL into a non-read statement or a multi-statement query, the read-query validator rejects it before execution.
Limits¶
- The open-source agent does not include a built-in row-filter or SQL-injection review plugin.
- The framework does not define a required policy schema beyond
enabled,provider, and the optionalprincipal.*pre-check convention. - CLI requests do not have HTTP headers. Request-scoped API principal input is available through the API auth context. For CLI or custom deployments, populate
AppContext.principalthrough the relevant auth or runtime integration. - Do not put
user_idinsideX-Datus-Principal;user_idis reserved forX-Datus-User-Id.