nl2sql-analytics
AGENTS.md0 starsChanged 49 days agoApache-2.0View on GitHub
The shared working agreement is .github/AGENTS.md. It carries the architecture rules, the gate contract, the fleet invariants, the falsification discipline, versions and house style, and it holds in every repository here. Read it first. This file carries only what is specific to this one. NL2SQL Semantic Analyst (H1): Governed NL-to-SQL analyst: certified semantic layer, deterministic SQL, cited answers. Rendered from hex-service-template, so it starts at reference parity rather than converging toward it. Package nl2sql_analytics, environment prefix NL2SQL, region asia-southeast1. docs/runbook.md…
- Reads credentials
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,700 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository6 other files it gives its agents.
The file
We split this AGENTS.md into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 30
After any dependency change, run make lock and commit both lockfiles.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
nl2sql-analytics
The shared working agreement is .github/AGENTS.md.
It carries the architecture rules, the gate contract, the fleet invariants, the
falsification discipline, versions and house style, and it holds in every repository
here. Read it first. This file carries only what is specific to this one.
What this is
NL2SQL Semantic Analyst (H1): Governed NL-to-SQL analyst: certified semantic layer, deterministic SQL, cited answers.
Rendered from hex-service-template, so it starts at reference parity rather than converging
toward it. Package nl2sql_analytics, environment prefix
NL2SQL, region asia-southeast1.
docs/runbook.md and docs/onprem-migration.md are operational and never restate product
behaviour. docs/practices-audit.md records this repository's
per-check verdict, and it is the file to update when a gap closes.
Commands
make install # locked install from requirements-dev.lock, then the project with --no-deps
make gate # ruff check + ruff format --check + mypy src + pytest -m 'not integration' + eval
make audit # pip-audit over both lockfiles (needs network; CI runs the same two commands)make audit is the one step that needs a vulnerability feed, which is why it is separate
locally and a hard-failing job in CI rather than an advisory one.
After any dependency change, run make lock and commit both lockfiles. An uncommitted resolution
is a version set nobody reviewed.
Architecture
Hexagonal, ports and adapters:
src/nl2sql_analytics/domain/is PURE stdlib.kernel.pyholds the vertical-neutral machinery;models.pyholds this vertical's artifacts. A fork building a different vertical rewritesmodels.pyand leaveskernel.py.ports/holds@runtime_checkableProtocols, re-exported once fromports/__init__.pywith thePORT_PROTOCOLSmap, plusports/identity.py: what an identity adapter declares about the end-user authentication it provides, and the refusal type that carries a status and a reason.adapters/{local,gcp,onprem}/are the three families.localis SDK-free and actually works;onpremis a placeholder that RAISES rather than pretending.config.pyresolves the profile and binds every port.config/settings.yamlcarries the binding table, so switching a port is configuration, not a code edit.agent/is the optional-but-scaffolded agent surface: plain tool callables plus the A2A card. It imports with no ADK and no cloud SDK;build_function_tools()is the only runtime seam.tests/splits intounit/,contract/,integration/andfixtures/. Integration modules are marked sopytest -m 'not integration'deselects them, and a test dropped intotests/root failstests/unit/test_test_layout.py.scripts/is the demo surface (seescripts/README.md): the scripted arc, a static renderer, a live click-through server, the presenter walkthrough that doubles as the self-test, the executable portability claim and the offline documentation checker. It is importable from the suite (pythonpath = ["scripts"]) and excluded from the serving image.ui/is the embeddable micro-frontend. One policy module and one server-side identity module are its whole security boundary. Runmake drop-uiif this repo has no user-facing surface.
Invariants (a change that breaks one of these is a defect, not a trade-off)
- Born fail-closed.
add_loopback_exposure_guardis bound at MODULE scope inapi/app.py, because the DockerfileCMDandmake run-apiserve the app OBJECT: a bound that lives only inmain()never runs in a shipped process.tests/unit/test_serving_path_exposure.pyis the standing gate. - The exposure guard is derived from the IDENTITY BINDING, and from nothing else. An end-user
route is authenticated when the bound identity adapter can produce a verified principal without
trusting a header the client wrote, and the adapter DECLARES that (
ports/identity.py:VERIFIED/CLIENT_ASSERTED/UNIMPLEMENTED, defaulting to client-asserted when silent).NL2SQL_S2S_TOKENmay never enter that decision: it authenticates a calling SERVICE and no end user, and while it did, SETTING it switched the guard off for the end-user routes it was protecting and a LAN peer with no credential got the seeded approver persona and a real triage decision.tests/unit/test_end_user_auth_posture.pywalks the guard's argument through the constants it names and fails the build if a credential reappears at any depth;scripts/prove-exposure-matrix.shin the template drives the whole matrix over a real socket. - The one adapter that declares
VERIFIEDmust EARN it, and it is the one adapter that may not go untested.adapters/gcp/identity.pycallsid_token.verify_tokenwithaudience=(the configuredNL2SQL_IAP_AUDIENCE, three-state: unset or emptied REFUSES, becauseaudience=Nonemeans the audience is NOT verified and accepts any Google-signed token from any project) andcerts_url=(IAP's own key set, not google-auth's OAuth2 default), checks the issuer itself (verify_tokendoes not), and WRAPS both the verifier call and the lazy import so no caller-supplied header can become a 500.MalformedErroris aValueError, not anIdentityError. Caller faults are 401 with the reason kept in the log; deployment faults (no audience, no verifier installed) are 503 naming the fix.tests/unit/test_iap_identity.pyruns in everymake gate;tests/unit/test_iap_crypto_matrix.pyruns the REAL verifier over locally minted assertions and is required by theiap-verifierCI job and by the template's render gate, which fail if it skips. There is no# pragma: no coveron this adapter. - Interactive docs are a relaxation, not a constant.
/docs,/redocand/openapi.jsonare registered only whenexposure_profileis the deliberatelocal. Undergcpthe loopback guard has stood down and the process binds every interface, so an uncredentialed peer was receiving the whole route inventory and every schema; the routes are ABSENT there rather than guarded, because a guard the profile has switched off is no guard. - One profile read, three states, refused at import.
config.PROFILE_CHOICEresolves once at import into aProfileChoice. UNSET is NO CHOICE, not a silentlocal; SET-AND-EMPTY raises so it cannot inherit the unset behaviour; SET-AND-UNKNOWN raises. Both raises kill the process before it can serve a request. Onlyconfig.pymay readNL2SQL_PROFILE(mentioning it in a refusal message is wanted, reading it is the defect);tests/unit/test_profile_single_source.pyfails the build if any other module re-derives it, because a permissive default gets reintroduced one module at a time. - Two derived postures, never one string. Relaxations (CORS, the
X-Dev-Personaallowed header, HSTS, the S2S scheme) key offProfileChoice.exposure_profile, which isunconfiguredwhen nobody chose. The loopback bound keys offProfileChoice.bind_profile, which islocalwhen nobody chose. They fail closed in OPPOSITE directions, so a single effective-profile string would harden one and weaken the other. The seeded-persona adapter refuses to construct unlesslocalwas chosen deliberately. - The audit trail is anchored, not just chained.
audit_anchor_pathwrites the chain head to a file on another volume. The chain catches an edit, a deletion or a reorder; only the anchor catches a truncated tail.tests/unit/test_audit_anchor.pyproves both, including the control case that fails without it. - Three-state environment reads, everywhere, enforced, in BOTH languages. In Python use
hex_service_kit.netdefaults.read_env_setting;tests/unit/test_three_state_env_reads.pywalks the AST ofsrc/,scripts/andeval/and fails the build on any two-stateos.environ.get/os.getenvread that is neither an exact-match comparison against a literal nor listed with a written reason. Inui/usereadEnvSettingfromui/lib/env-setting.mjs;ui/tests/three-state-env-reads.test.mjsscans every shipped.mjs/.ts/.tsxwith the same rule and the same two escapes. Both halves are needed: a guard that only watched the PROFILE variable is how a two-state read of a different one stayed invisible, and a guard that only parsed Python is howenv.UI_TENANT_ORIGINS || "*"survived the entire gate. - Rule R8: escalations are ROUTED, never merely flagged. Setting
requires_human_reviewand callingReviewRouterPort.routeis one act.api/app.py,cli/main.pyandagent/tools.pyall route in the same call that produced the result.tests/unit/test_review_routing.pyis the standing gate; a local router that silently did nothing would let a producer ship R8 unwired and green. - The consequential decision is deterministic. The severity band and the escalation come from pure stdlib code and are replayable. An LLM may narrate the result; it may never produce the band.
- The demo is code, and every narrated claim is checked. A step lives in
demo.STEPSand inwalkthrough.CHECKS, andtests/unit/test_demo_surface.pyholds the two equal, so a claim the demo makes but nobody verifies cannot exist.make demo-selftestruns the whole arc headless in CI. Do NOT move the demo intomake gate: the gate proves the service and must stay fast and offline, and the demo has its own required check. - The browser never asserts who it is. In
ui/, every client-supplied actor, tenant, role, ACL and authorization header is discarded before forwarding, identity is resolved server-side, and the service credential stays on the server. Framing and CORS are allowlists that refuse a wildcard, and every variable behind them is read in three states: unset takes the documented restrictive default, EMPTIED refuses.UI_PROFILErefuses rather than seeding a dev persona when nobody chose;UI_FRAME_ANCESTORSandUI_TENANT_ORIGINSrefuse fromnext.config.mjs, so the refusal is a build/boot refusal rather than a surprise on a later request. The one deliberate exception is the OUTBOUNDAGENT_S2S_TOKEN, exempted with a written reason because the receiver decides whether an uncredentialed call is acceptable. ui/, its dependabot ecosystem and its CI job agree, in both directions. Present together or absent together.make drop-uiis the one step that keeps them consistent, andtests/unit/test_ui_surface.pyfails the gate until they do.
Extending
CONTRIBUTING.md carries the file-by-file touch list for both walkthroughs, with the test that
enforces each row. The short version:
- A new adapter: the class under
adapters/<family>/(one constructor shape,Adapter(settings), cloud imports inside the method), the samemodule:Classtarget inconfig.DEFAULT_BINDINGSANDconfig/settings.yaml(tests/unit/test_settings_file.pyfails if the two disagree), plus any new variable in.env.example. - A new port: it must be registered in FIVE places, or it runs with no enforcement at all:
ports/__init__.py(PORT_PROTOCOLS),config.DEFAULT_BINDINGS, aContaineraccessor,config/settings.yaml, and aPortCaseintests/contract/canonical.py. Then bind it in all three families.tests/contract/test_port_parity.pyasserts set equality across the five. - A new surface: whatever it is, it routes escalations (R8) and it minimises what it hands a
model. An agent tool also needs its skill in
agent/agent_card.py; the card and the tool table are compared for set equality. - A new demo step: the
Stepand its_step_<key>method inscripts/demo.py, plus the matching entry inwalkthrough.CHECKS. Put the numbers the check reads in the step'sfactsdict, never only in the rendered rows: a check that parses prose breaks on a wording change.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

