<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmitry</title>
    <description>The latest articles on DEV Community by Dmitry (@ivenin).</description>
    <link>https://dev.to/ivenin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081447%2F03fb9053-95ff-49e1-bd97-3107a6cd1868.png</url>
      <title>DEV Community: Dmitry</title>
      <link>https://dev.to/ivenin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ivenin"/>
    <language>en</language>
    <item>
      <title>I Didn't Want My AI Agent to Have a Database Password, So I Built a Gateway</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Mon, 17 Aug 2026 11:10:00 +0000</pubDate>
      <link>https://dev.to/ivenin/i-didnt-want-my-ai-agent-to-have-a-database-password-so-i-built-a-gateway-3770</link>
      <guid>https://dev.to/ivenin/i-didnt-want-my-ai-agent-to-have-a-database-password-so-i-built-a-gateway-3770</guid>
      <description>&lt;p&gt;The first version of the idea was embarrassingly simple.&lt;/p&gt;

&lt;p&gt;Give the agent a database URL, let it write SQL, run the query, and send the rows back to the model.&lt;/p&gt;

&lt;p&gt;It would have made a nice demo. It also would have made a terrible system.&lt;/p&gt;

&lt;p&gt;The problem wasn't only that a model might produce a &lt;code&gt;DROP TABLE&lt;/code&gt;. A read-only user can still run a query that scans half a warehouse, hold connections open, join against a table it was never supposed to see, or return far more data than the agent actually needs.&lt;/p&gt;

&lt;p&gt;And a database password is still a database password, even when it is hidden inside an agent configuration file.&lt;/p&gt;

&lt;p&gt;That was the starting point for &lt;a href="https://github.com/sickagent/n0" rel="noopener noreferrer"&gt;n0&lt;/a&gt;: an open-source Go platform that sits between AI agents and enterprise data. The agent gets a small set of useful tools. The gateway owns authentication and tenant context. A separate query service decides whether the SQL is safe to execute.&lt;/p&gt;

&lt;p&gt;This is a note about the architecture, but also about the decisions behind it. The project is working software, not a claim that every production concern has been solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;I keep coming back to one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent decides what it wants to ask. The platform decides whether it is allowed to ask it and how the request is executed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rough request path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI agent
   │
   │ MCP / Streamable HTTP
   ▼
Agent Gateway
   │  JWT, tenant context, tool routing
   ├──────────────► Meta Service ─────► PostgreSQL
   │                 workspaces,        metadata
   │                 connections, schema
   │
   └──────────────► Query Engine ─────► NATS JetStream
                     SQL sandbox,       asynchronous jobs
                     result lifecycle
                           │
                           ▼
                    Connection Manager
                           │
                           ▼
                 PostgreSQL / MySQL / ClickHouse / ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few more boxes in the repository, but these are the boundaries that matter for an agent.&lt;/p&gt;

&lt;p&gt;The MCP server lives in the Agent Gateway. It does not open a database connection, inspect credentials, or implement a second query executor. It translates MCP calls into the same internal clients used by the REST API.&lt;/p&gt;

&lt;p&gt;That last part is intentional. I didn't want security rules to slowly diverge between "the API path" and "the AI path".&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP is not the security boundary
&lt;/h2&gt;

&lt;p&gt;MCP is a useful way to describe capabilities to an agent. It gives the client a standard way to discover tools and call them.&lt;/p&gt;

&lt;p&gt;It does not answer the important questions for a data platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is calling?&lt;/li&gt;
&lt;li&gt;Which tenant do they belong to?&lt;/li&gt;
&lt;li&gt;Which connection can they use?&lt;/li&gt;
&lt;li&gt;Which tables are allowed?&lt;/li&gt;
&lt;li&gt;Is this query safe and bounded?&lt;/li&gt;
&lt;li&gt;Where does the result live while the query is running?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions belong to the gateway and the services behind it.&lt;/p&gt;

&lt;p&gt;In n0, the MCP endpoint is exposed at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8083/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It goes through the same JWT middleware as the REST API. The tenant ID used for internal requests comes from the verified JWT context, not from a &lt;code&gt;tenant_id&lt;/code&gt; argument supplied by the agent.&lt;/p&gt;

&lt;p&gt;That distinction is easy to miss. If a tool accepts both &lt;code&gt;connection_id&lt;/code&gt; and &lt;code&gt;tenant_id&lt;/code&gt;, the model can accidentally—or deliberately—ask for a different tenant's data. The public tool doesn't accept that field at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools are deliberately boring
&lt;/h2&gt;

&lt;p&gt;The current MCP server exposes six tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_schema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns the schema visible for a connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;submit_query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates an asynchronous read-only query job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_query_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checks the state of a query job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_query_result&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches a paginated result page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_connections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists connections without returning credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_workspaces&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists workspaces in the current tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no &lt;code&gt;execute_sql_now&lt;/code&gt; tool. There is no tool that returns a connection string. There is no tool that lets the model choose a different tenant.&lt;/p&gt;

&lt;p&gt;The boringness is a feature. Tools are part of the security model, not just conveniences for prompting.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real tool call
&lt;/h2&gt;

&lt;p&gt;Once the gateway is running, a client can call &lt;code&gt;submit_query&lt;/code&gt; with a normal MCP JSON-RPC request. &lt;code&gt;$TOKEN&lt;/code&gt; can be a user JWT or an agent token issued by n0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8083/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json, text/event-stream'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "submit_query",
      "arguments": {
        "connection_id": "conn_123",
        "sql": "SELECT customer_id, sum(amount) AS revenue FROM public.orders GROUP BY customer_id"
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"structuredContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"job_456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent polls &lt;code&gt;get_query_status&lt;/code&gt;, then asks for pages with &lt;code&gt;get_query_result&lt;/code&gt;. This is a better fit for analytical work than holding one HTTP request open while a warehouse does its thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implementation is a thin adapter
&lt;/h2&gt;

&lt;p&gt;The MCP registration is intentionally small. The official Go SDK handles the protocol details and typed tool schemas; the handler calls the existing Query Engine client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"submit_query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Submit a tenant-scoped read-only SQL query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mcpSubmitQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is not the registration. It is the context propagation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queryCli&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SubmitQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SubmitQueryRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;mcpTenantID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;ConnectionId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Sql&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP request context has already passed through JWT verification. The handler doesn't trust a tenant field from the tool arguments and doesn't try to reimplement authorization in the MCP package.&lt;/p&gt;

&lt;p&gt;This also means the same internal client can be exercised from REST, MCP, and tests. Fewer paths are good. Fewer security models are better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to the SQL?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;submit_query&lt;/code&gt; only creates a job. Query Engine is the component that validates and executes the statement.&lt;/p&gt;

&lt;p&gt;The current sandbox is intentionally conservative. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepts one &lt;code&gt;SELECT&lt;/code&gt; statement;&lt;/li&gt;
&lt;li&gt;rejects DDL and DML such as &lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;DROP&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;rejects multiple statements and locking reads;&lt;/li&gt;
&lt;li&gt;adds a finite &lt;code&gt;LIMIT&lt;/code&gt; when one is missing;&lt;/li&gt;
&lt;li&gt;rejects excessive or malformed limits;&lt;/li&gt;
&lt;li&gt;checks referenced tables against the connection policy;&lt;/li&gt;
&lt;li&gt;can inject a tenant predicate when a policy defines a tenant column;&lt;/li&gt;
&lt;li&gt;propagates an execution timeout to the database driver.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source database should still use a read-only role. Application checks are not a replacement for database permissions. They are another layer.&lt;/p&gt;

&lt;p&gt;This is the part where I resist the temptation to say "the query is safe". The honest version is: the query passed the checks we currently enforce, and the database role and network boundaries are still important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why make queries asynchronous?
&lt;/h2&gt;

&lt;p&gt;For a tiny demo, synchronous execution is simpler. For a platform, it creates a pile of awkward edge cases.&lt;/p&gt;

&lt;p&gt;What happens when the client disconnects while the database is still working? What happens when the result is larger than the response body? What happens when the worker restarts after the query has been accepted? How do you retry without running the same expensive query twice?&lt;/p&gt;

&lt;p&gt;Turning the request into a job gives the system somewhere to put those answers.&lt;/p&gt;

&lt;p&gt;The flow is roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent Gateway authenticates the caller and submits the request.&lt;/li&gt;
&lt;li&gt;Query Engine publishes a durable job through NATS JetStream.&lt;/li&gt;
&lt;li&gt;A worker validates and executes the SQL through Connection Manager.&lt;/li&gt;
&lt;li&gt;Job state and results are persisted separately from the gateway process.&lt;/li&gt;
&lt;li&gt;The agent polls status and reads paginated results.&lt;/li&gt;
&lt;li&gt;Terminal events are sent through the audit pipeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is more code than &lt;code&gt;db.QueryContext&lt;/code&gt;. It is also much easier to reason about once there is more than one user and more than one query running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The awkward parts I am not hiding
&lt;/h2&gt;

&lt;p&gt;The current version has working MCP support, but it is not a finished cloud product.&lt;/p&gt;

&lt;p&gt;There are still open areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vault-backed runtime credential leases;&lt;/li&gt;
&lt;li&gt;PostgreSQL RLS for metadata tables;&lt;/li&gt;
&lt;li&gt;distributed rate limits and quotas;&lt;/li&gt;
&lt;li&gt;production Kubernetes and high-availability manifests;&lt;/li&gt;
&lt;li&gt;agent token ownership verification and revocation lifecycle;&lt;/li&gt;
&lt;li&gt;dynamic capability discovery for plugins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The MCP transport is stateless so gateway replicas do not depend on an in-memory session store. That keeps the first deployment model simple, but it also means the rest of the production story—rate limiting, revocation, observability, and operational policy—still needs to be designed at platform level.&lt;/p&gt;

&lt;p&gt;I prefer saying this out loud. A working endpoint is not the same thing as a completed security program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it locally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sickagent/n0.git
&lt;span class="nb"&gt;cd &lt;/span&gt;n0
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
make up
make migrate-up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local stack exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web Admin at &lt;code&gt;http://localhost:3000&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;REST API at &lt;code&gt;http://localhost:8083&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;MCP at &lt;code&gt;http://localhost:8083/mcp&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository README walks through registration, workspace creation, adding a connection, discovering its schema, and submitting a query. The architectural trade-offs are documented in &lt;a href="https://github.com/sickagent/n0/blob/main/ADR-001-n0.md" rel="noopener noreferrer"&gt;ADR-001&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small takeaway
&lt;/h2&gt;

&lt;p&gt;MCP makes it easier for an agent to discover and call tools. It does not make direct database access safe.&lt;/p&gt;

&lt;p&gt;The useful pattern, at least for this project, is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;standard agent protocol
        +
strong identity and tenant context
        +
default-deny query policy
        +
asynchronous execution
        +
database-level least privilege
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those layers is perfect on its own. That is exactly why they should not be collapsed into one clever prompt, one regex, or one database role.&lt;/p&gt;

&lt;p&gt;If you are building an AI data analyst, the interesting question is probably not "which model writes the best SQL?"&lt;/p&gt;

&lt;p&gt;It is "what is the smallest, most boring capability I can safely give the model?"&lt;/p&gt;

&lt;p&gt;That is the question n0 is trying to answer.&lt;/p&gt;

&lt;p&gt;The code is on &lt;a href="https://github.com/sickagent/n0" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Feedback, issues, and arguments about the boundaries are welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>go</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
