AGENTS.md
Rama main · réplica de lectura del repositorio original.
# AGENTS.md — proxy-monster
Entry point for anyone picking up this project. Read this first.
## What it is
proxy-monster is a self-hosted, open-source database access-control proxy for
MySQL and PostgreSQL. It enforces column-level access control — deterministic,
role-based masking and deny — and is lineage-aware: it follows sensitive values
through SQL expressions, functions, subqueries, and joins. Clients connect with
their normal tools (psql, mysql, JDBC) over the native wire protocol; the proxy
masks per role and denies anything it cannot prove safe (**fail-closed**).
The goal is a transparent, in-VPC, self-hostable access-control proxy you own
and can extend, friendly to CLIs and native clients.
## Engine priority
MySQL is the primary target and the correctness bar for shipping — build and
verify it first. PostgreSQL support is experimental and developed alongside;
today PostgreSQL also serves as proxy-monster's own control-plane store.
PostgreSQL-only edge cases — its transactional-DDL behaviors (in-transaction DDL
visibility, Sync/implicit-transaction rollback, deferred-constraint COMMIT
failure) — may be carried as documented known limitations rather than block a
MySQL milestone. Keep MySQL and PostgreSQL separate when splitting work or
weighing review findings, and weight MySQL accordingly.
## Layout
Modules:
- `goproxy/` — Go data-plane wire proxy: token auth, the per-statement `Decide`
call, inline result masking, and the target-DB broker. Three levels: the
engine, the target it connects to, and a connection to that target. One
package per engine (`mysql/`, `postgres/`, `athena/`) holds its provider, SQL,
table detail, and wire broker; `sqldb/` is what MySQL and PostgreSQL share
through database/sql (introspection, the schema refetcher, table detail,
pools); `relay/` is the engine-agnostic Decide, masking and serving; `spi/`
defines the contracts between them. `cmd/goproxy` builds the provider
registry; generic packages never import an engine package.
- `cpgo/` — Go front door of the control plane: owns its public HTTP and gRPC
ports, runs the Kotlin control plane as a child on loopback, and forwards to
it what Go does not serve yet
([docs/cp-go-migration.md](./docs/cp-go-migration.md)).
- `control-plane/` — Kotlin control plane: identity and roles, Cedar
authorization, the catalog, the per-statement decision, and the admin +
console API (HTTP and gRPC).
- `analyzer/` — `engines/` is the one list of target engines: each engine's
facts, canonical names, and sqlglot-go lineage analyzer, which emits each
statement's required grants. `jvm/` is its JVM FFM binding.
- `engine/` — shared Kotlin enforcement code the control plane calls: the
JVM-side wrapper around the sqlglot-go analyzer, system classification (the
dangerous-function and system-catalog manifests), SQL normalization, and the
mask functions used when a stored approval result is viewed.
- `auth/` — Kotlin OIDC login and the MCP OAuth authorization server.
- `auditmon/` — Go audit-trail monitor: verifies the hash chain, anchors
off-box, detects anomalies, and exports to a SIEM.
- `pmon/` — Go client daemon: connect with a saved password while it brokers a
short-lived token upstream. Its menu-bar app is `pmon/tray/`.
Connection-string formatters and brokers use its
[provider SPI](./pmon/AGENTS.md).
- `auto-approver/` — `pmon-auto-approver-agent`, the TypeScript auto-approval
agent cp-go runs once per review, compiled with bun into the server image.
- `sql-action/` — the GitHub Action that files a pull request's SQL files, and
`pm-ci`, the Go binary it runs
([docs/sql-from-github.md](./docs/sql-from-github.md)).
- `mysqlwire/` — Go MySQL wire-protocol codec library (shared by `goproxy` and
`pmon`).
- `proto/` — protobuf contracts: the proxy↔control-plane gRPC surface and the
analyzer FFM boundary.
- `web/` — the Next.js console (editor, policies, access, audit, admin).
- `deploy/` — sample seed SQL for the compose target DBs.
- `docs/` — per-workstream design docs.
Key docs:
- [`README.md`](./README.md) — project front door.
- [`ARCHITECTURE.md`](./ARCHITECTURE.md) — the components, topology, trust
boundaries, and ports.
- [`DESIGN.md`](./DESIGN.md) — the design decisions and the
decision-to-enforcement flow.
- [`INSTALL.md`](./INSTALL.md) — install, run locally, and deploy (local + AWS).
- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — how to build, test, and contribute.
- [`SECURITY.md`](./SECURITY.md) — how to report a vulnerability.
- [`KNOWN_LIMITATIONS.md`](./KNOWN_LIMITATIONS.md) — accepted caveats and gaps.
- [`docs/README.md`](./docs/README.md) — the design-doc index, plus a summary of
what's built.
## Stack
- The control-plane is Kotlin/JVM; the wire proxy and the lineage analyzer are
Go. Kotlin/JVM was the original choice, for an in-process JVM lineage engine;
after the engine moved to sqlglot-go the system is being ported to Go
incrementally, and the control-plane is the last Kotlin component. The
analyzer still runs inside the JVM through a Foreign Function & Memory binding
to a Go c-shared library (JDK 24 — the Java compiler is pinned to
`--release 24` and every module targets JVM 24).
- Lineage: the sqlglot-go probe in `analyzer/` resolves column lineage — it
parses each statement and traces which source columns every output and
predicate derives from. proxy-monster owns the probe; sqlglot-go is a library
dependency.
- Enforcement: masking and deny. When a query touches masked columns the
analyzer rewrites `SELECT *` to an explicit column list (a faithful,
semantically-identical rewrite) so mask ordinals are fixed; the proxy then
masks those columns inline on the result stream. Anything the analyzer cannot
prove safe is denied. Masking is result-stream rewriting — the query is not
rewritten to inject mask expressions.
- Identity: OIDC. The IdP's group claim provisions local group membership (via
SCIM or JIT on first login); local groups map to roles through the
`group_role` map. The IdP never mints roles directly. Okta is the reference
provider; any OIDC IdP works ([docs/auth-model.md](./docs/auth-model.md)).
- Wire auth: SSO login plus a local broker daemon (`pmon`). Saved connections
use a fixed localhost password while the daemon injects a short-lived token on
the upstream hop. The daemon does not renew that token — a login lasts one
token TTL, then `pmon login` again (see
[DESIGN.md](./DESIGN.md#identity-and-broker)).
- JIT elevation: a time-boxed, revocable grant of a role to a user (an
`access_grant`). The grant just adds to the user's effective roles, so the
same per-query decision applies — the engine has no separate elevation path
(see [DESIGN.md](./DESIGN.md#jit-elevation-and-approval)).
- Planes: the Kotlin control-plane owns a Postgres store (identity, policy,
catalog, grants, audit); the Go data-plane proxy holds no store — it connects
only to its target DB and reads each decision from the control-plane over
gRPC.
- Web console (`web/`): a SQL editor plus admin for datasources, policies,
access, and audit, in Next.js.
## New control-plane code: Go first
The control plane is moving to Go slice by slice
([docs/cp-go-migration.md](./docs/cp-go-migration.md)). New control-plane code
goes in `cpgo/`, not `control-plane/`, unless it only extends a Kotlin path that
has not moved yet. Kotlin that stays is the smallest change that keeps a
not-yet-moved path working.
When Kotlin must call Go, use gRPC with a proto contract, never hand-written
JSON DTOs on both sides:
- One service per slice (e.g. `GitHubSource`), in its own file under
`proto/src/main/proto/`. The generated types are the data classes on both
sides.
- cp-go serves it on a loopback gRPC listener it starts before the Kotlin child,
and passes the address to the child. The per-boot `PM_CP_INTERNAL_TOKEN`
travels in gRPC metadata. Register the Go side on `cpgo/gobridge`; build the
Kotlin stub on `CpGoBridge.channel`.
- Forward whole operations, which Go runs in its own transaction. A single row
written inside a Kotlin transaction (a `request_event`, say) stays a Kotlin
insert until that call site moves; an RPC there would split the transaction.
- A service exists only while its slice is half-moved: delete it when the Kotlin
caller moves to Go, and list it in `docs/cp-go-migration.md` until then.
Go calling Kotlin stays on the existing `/internal/*` HTTP bridge
(`cpgo/bridge`), which only shrinks.
## Control-plane HTTP API
There is no API reference and no OpenAPI spec. The routes are the surface and
the `@Serializable` Kotlin data classes in each owning file are the
request/response reference. Handler names do not track their path prefixes, so
the map of which file owns which prefix — and the gate each one calls — is in
[docs/authz-model.md](./docs/authz-model.md#http-auth-gates).
An authenticated session alone is never authorization. A route states its
requirement by which gate helper it calls (`requireApi`, `requireAdmin`,
`requireAuthz`, `requireScimAuth`). `PM_AUTH_DEBUG` is an authentication
setting: it adds a login method (`POST /auth/debug`, any principal with any
roles, minted as a real session), and the gates read that session like any
other.
Errors are never English prose on the wire: a route responds
`ApiError(code, params)` with a stable dot-namespaced code the web looks up as
an i18n message key. `Scim.kt` is exempt — its body follows the SCIM 2.0 spec.
See [docs/l10n.md](./docs/l10n.md).
## Conventions
Full list in [CONTRIBUTING.md](./CONTRIBUTING.md#conventions). The three that
most often get violated:
- Comments and docs are not a diary, and this is a **security** rule before it
is a style one: this repository is public. A comment explains what the code
cannot say — an outside constraint, a non-obvious failure mode, why not the
obvious alternative. It must not narrate how the code came to be: no review
findings, no what-was-tried, no "previously X", no debugging account. That
narration is what carries host names, paths, internal tooling, and topology
into a public repository. History goes in the commit message, which is also
public — the same rule about naming internal systems applies there.
- l10n is non-negotiable. Every user-facing string is localized (English and
Korean today). The server returns a stable dot-namespaced code via `ApiError`,
never English prose; every message key must exist in every locale under
`web/messages/<locale>/` ([docs/l10n.md](./docs/l10n.md)).
- Fail-closed through Cedar, not a hardcoded deny. When the analyzer cannot
prove a statement safe, route it through the deny-by-default gate
(`exception.unanalyzable` / `exception.unmaskable`) so a datasource can
override it while the production floor stays closed. Coverage gaps are
security gaps.
## Build, test, run
The toolchain is pinned via mise (`mise.toml`), which also defines the tasks:
```sh
mise run dev # the whole local stack
mise run verify # the whole gate: lint, JVM tests, Go tests, web tests + build
```
`mise tasks ls` lists the rest, including per-component tasks for running one
piece against a stack you started by hand. The gate is mandatory before pushing
and its DB-backed tests need Docker — details, and the flags that change that,
in [CONTRIBUTING.md](./CONTRIBUTING.md#build-test-run). Install and deploy:
[INSTALL.md](./INSTALL.md).
## Releases
release-please cuts four trains (server, `pmon`, `pm-ci`, `mysqlwire`) from one
config; see [CONTRIBUTING.md](./CONTRIBUTING.md#releases). The paths a commit
touches decide its train.
Never force a version with an empty commit carrying a `Release-As:` footer. An
empty commit touches no path, so release-please counts it toward every train:
`Release-As: 0.1.21` meant for the server also set `pmon` to 0.1.21. Put the
footer on a commit that edits a file in that train's directory, such as its
`CHANGELOG.md`. The one such commit already on `main` is fenced off by
`last-release-sha` in `release-please-config.json`; move that floor forward if
another ever lands.
## Design docs
Index and what's built: [docs/README.md](./docs/README.md). House style:
[CONTRIBUTING.md](./CONTRIBUTING.md#conventions).