diff --git a/CHANGELOG.md b/CHANGELOG.md index e357d729..71dc55a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v2.0.0] + +- Add a fixed two-replica Kubernetes deployment for stateless read-only MCP traffic backed by shared PostgreSQL metadata and a separate audit database. Keep B5 transactions, stateful sessions and event replay disabled in this topology. +- Add a management API to enroll one regular PostgreSQL table's explicit columns under a locked catalog proof and policy revision CAS. Enrollment SQL is analyzed without execution; unsupported native, join, view and wildcard cases fail closed. +- Bound concurrent RBAC bootstrap and B2 activation retries, and rehearse two-node failover, dependency outages, paired backup restore and a maintenance-window rollback to v1.9.0. The [v2.0.0 acceptance record](docs/v2.0.0-acceptance.md) limits these claims to the tested synthetic environment. + ## [v1.9.0] - Add an onboarding flow for Dify 1.17.1 and MCP Inspector 0.16.8 with client-reachable gateway URLs and key-free connection instructions. diff --git a/Dockerfile b/Dockerfile index 803a8f4b..3c189949 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM golang:1.26.9-bookworm AS build -ARG VERSION=v1.9.0 +ARG VERSION=v2.0.0 # Overridable module proxy for restricted networks, e.g. # `docker build --build-arg GOPROXY=https://goproxy.cn,direct`. ARG GOPROXY=https://proxy.golang.org,direct @@ -48,7 +48,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build mkdir -p /out \ FROM debian:bookworm-slim AS runtime -ARG VERSION=v1.9.0 +ARG VERSION=v2.0.0 ARG REVISION LABEL org.opencontainers.image.title="AgentSQL" \ org.opencontainers.image.source="https://github.com/cuipengdba/agentsql" \ diff --git a/Makefile b/Makefile index 2efb68b2..4efb8b02 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ BINARY_DIR := bin -VERSION ?= v1.9.0 +VERSION ?= v2.0.0 GO_VERSION ?= 1.26.9 GOPROXY ?= https://goproxy.cn,direct SOURCE_DATE_EPOCH ?= 1704067200 diff --git a/README.md b/README.md index b69f7ec0..d92340b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AgentSQL -![Version v1.9.0](https://img.shields.io/badge/Version-v1.9.0-blue) +![Version v2.0.0](https://img.shields.io/badge/Version-v2.0.0-blue) [![Go](https://img.shields.io/badge/Go-1.26-00ADD8?logo=go)](go.mod) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue)](LICENSE) [![Commercial License](https://img.shields.io/badge/License-Commercial-orange)](COMMERCIAL-LICENSE.md) @@ -13,9 +13,9 @@ AgentSQL(中文品牌名:智盾,控制台显示为「AgentSQL 智盾控制 v0.6.0 加入治理控制台、审批单次执行和 Vastbase G100 的受限只读适配;v0.7.0 加入独立的 MCP `2026-07-28` 入口与任务级临时只读授权;v0.8.0 加入异人审批;v0.9.0 为新申请加入 1–10 次结果预算、管理审计投递状态和策略离线预检。v1.0.0 收紧列策略的失败关闭,并加入可选的 Agent 与人员双身份 HTTP MCP 验证。v1.1.0 将双身份扩展到审批与 B5 会话,并加入委托令牌单独撤销。v1.2.0 增加可选的 MCP OAuth 受保护资源入口。v1.3.0 增加默认关闭的 MySQL 8.0/8.4 单表 InnoDB SELECT 列授权子集。v1.4.0 在 PostgreSQL metadata 部署中共享控制台密码和 LDAP 登录尝试预算,并验证双网关审计故障恢复。自 v0.5.0 起代码采用 Apache-2.0,历史 v0.4.0 发布物仍按其原许可使用。 -v1.5.0 对达梦、金仓和 Vastbase 的指定实例深入验证真实 MCP 客户端、权限撤销、断库恢复及审计。v1.6.0 在相同指定实例和受控只读子集内补齐故障关闭。v1.7.0 为瀚高 9.0.10 企业版 PG 模式增加独立 `highgo` 类型及有界只读路径,并为金仓 V9R1C10 PG 模式补充证书校验与凭据轮换。v1.8.0 在指定 Vastbase G100 Build 9 PG 模式实例验证 `verify-full`、证书和账号轮换、MCP 双客户端、脱敏与审计;主备高可用和完整生产支持仍未验收,见[发布说明](docs/release-notes-v1.8.0.md)。v1.9.0 增加 Dify / MCP Inspector 接入说明、复用 Agent 与数据源的目标 SQL 离线解释及真实调用审计定位;见[发布说明](docs/release-notes-v1.9.0.md)。 +v1.5.0 对达梦、金仓和 Vastbase 的指定实例深入验证真实 MCP 客户端、权限撤销、断库恢复及审计。v1.6.0 在相同指定实例和受控只读子集内补齐故障关闭。v1.7.0 为瀚高 9.0.10 企业版 PG 模式增加独立 `highgo` 类型及有界只读路径,并为金仓 V9R1C10 PG 模式补充证书校验与凭据轮换。v1.8.0 在指定 Vastbase G100 Build 9 PG 模式实例验证 `verify-full`、证书和账号轮换、MCP 双客户端、脱敏与审计;主备高可用和完整生产支持仍未验收,见[发布说明](docs/release-notes-v1.8.0.md)。v1.9.0 增加 Dify / MCP Inspector 接入说明、复用 Agent 与数据源的目标 SQL 离线解释及真实调用审计定位;见[发布说明](docs/release-notes-v1.9.0.md)。v2.0.0 增加固定双网关 Kubernetes 配置和 PostgreSQL 单表列策略正式登记 API,见[发布说明](docs/release-notes-v2.0.0.md)及[双网关操作指南](deploy/kubernetes/README.md)。 -**验证范围:**v1.0.0 的列策略、人员委托及真实客户端结果见[验收记录](docs/v1.0-acceptance.md);v1.1.0 的审批与 B5 双身份结果见[验收记录](docs/v1.1.0-acceptance.md);v1.2.0 的 OAuth 互通结果见[验收记录](docs/v1.2.0-acceptance.md);v1.3.0 的 MySQL 列授权边界见[验收记录](docs/v1.3.0-acceptance.md);v1.4.0 的共享登录预算与双节点恢复见[验收记录](docs/v1.4.0-acceptance.md);v1.5.0 的三款国产数据库指定子集深度验证见[验收记录](docs/v1.5.0-acceptance.md);v1.6.0 的故障关闭与恢复证据见[验收记录](docs/v1.6.0-acceptance.md);v1.7.0 的瀚高和金仓证据见[正式发布验收](docs/v1.7.0-release-evidence.md);v1.8.0 的 Vastbase TLS 范围见[本版验收](docs/v1.8.0-acceptance.md);v1.9.0 接入向导改进见[本版验收](docs/v1.9.0-acceptance.md)。厂商结论只覆盖指定实例、模式及合成数据。以下命令须以 [GitHub Release](https://github.com/cuipengdba/agentsql/releases/tag/v1.9.0)、签名及镜像匿名拉取验收为准。 +**验证范围:**v1.0.0 的列策略、人员委托及真实客户端结果见[验收记录](docs/v1.0-acceptance.md);v1.1.0 的审批与 B5 双身份结果见[验收记录](docs/v1.1.0-acceptance.md);v1.2.0 的 OAuth 互通结果见[验收记录](docs/v1.2.0-acceptance.md);v1.3.0 的 MySQL 列授权边界见[验收记录](docs/v1.3.0-acceptance.md);v1.4.0 的共享登录预算与双节点恢复见[验收记录](docs/v1.4.0-acceptance.md);v1.5.0 的三款国产数据库指定子集深度验证见[验收记录](docs/v1.5.0-acceptance.md);v1.6.0 的故障关闭与恢复证据见[验收记录](docs/v1.6.0-acceptance.md);v1.7.0 的瀚高和金仓证据见[正式发布验收](docs/v1.7.0-release-evidence.md);v1.8.0 的 Vastbase TLS 范围见[本版验收](docs/v1.8.0-acceptance.md);v1.9.0 接入向导改进见[本版验收](docs/v1.9.0-acceptance.md);v2.0.0 双网关与管理端列登记见[本版验收](docs/v2.0.0-acceptance.md)。厂商结论只覆盖指定实例、模式及合成数据。以下命令须以 [GitHub Release](https://github.com/cuipengdba/agentsql/releases/tag/v2.0.0)、签名及镜像匿名拉取验收为准。 ```text AI Agent → LLM / MCP Client → AgentSQL 网关 → PostgreSQL / MySQL @@ -38,6 +38,7 @@ AI Agent → LLM / MCP Client → AgentSQL 网关 → PostgreSQL / MySQL - v1.2.0 MCP OAuth:可选的 `/mcp/oauth` 受保护资源入口使用外部 OIDC/OAuth 授权服务器签发的资源专用访问令牌,并逐次绑定独立 Agent Key、用户映射、租户与本地权限;支持范围见发布说明。 - v1.3.0 MySQL 列授权:显式开启后,MySQL 8.0/8.4 指定 InnoDB 基表的受限单表 SELECT 使用独立只读业务账号与 PROCESS-only 元数据账号、表 ID 和 metadata lock 证明列权限;不扩大为所有 MySQL SELECT。 - v1.4.0 多网关登录预算:PostgreSQL metadata 部署对密码和 LDAP 登录使用共享的 60 秒 10 次预算;数据库故障返回 503,预算耗尽返回 429,SQLite 单实例仍使用本地预算。 +- v2.0.0 固定双网关部署:两个 Kubernetes 网关共享 PostgreSQL metadata 与独立审计库,分开保存 B2 实例标识;无状态 MCP 只读请求通过 Service 转发,升级与回退需维护窗口。管理端可对 PostgreSQL 闭合绑定模式的单张普通表登记列策略。公开 Live Demo 仍采用单网关合成数据环境。 ## 生态与兼容 @@ -79,17 +80,17 @@ AgentSQL 作为数据库中立的安全网关,当前原生支持 PostgreSQL v0.4.0 起提供 `linux/amd64` 与 `linux/arm64` 两种原生 glibc 包:`uname -m` 为 `x86_64` / `amd64` 时使用 amd64 包,为 `aarch64` / `arm64` 时使用 arm64 包。两者均要求 glibc 2.28+ 且 systemd 为 PID 1;安装器会自动选择本机架构、校验发布包外层与包内哈希,并安装为 systemd 服务。musl / Alpine 与 CentOS 7 仍不支持: ```bash -curl -fsSL https://github.com/cuipengdba/agentsql/releases/download/v1.9.0/install.sh | sudo sh -s -- install --version v1.9.0 +curl -fsSL https://github.com/cuipengdba/agentsql/releases/download/v2.0.0/install.sh | sudo sh -s -- install --version v2.0.0 ``` 管道和 CI 的非 TTY 输出不会显示自动生成的管理员密码;root 可在 `/etc/agentsql/agentsql.env` 查看。交互终端会在首次创建凭据时显示一次,也可显式加 `--show-password`。离线环境同时取得 tarball 与同名 `.sha256` 后,校验、解压并从包根安装: -以下是 v1.9.0 的 amd64 资产名示例;v0.4.0 起 arm64 主机使用同版本的 `-linux-arm64` 资产名。 +以下是 v2.0.0 的 amd64 资产名示例;v0.4.0 起 arm64 主机使用同版本的 `-linux-arm64` 资产名。 ```bash -sha256sum -c agentsql-v1.9.0-linux-amd64.tar.gz.sha256 -tar -xzf agentsql-v1.9.0-linux-amd64.tar.gz -cd agentsql-v1.9.0-linux-amd64 +sha256sum -c agentsql-v2.0.0-linux-amd64.tar.gz.sha256 +tar -xzf agentsql-v2.0.0-linux-amd64.tar.gz +cd agentsql-v2.0.0-linux-amd64 sudo ./install.sh install ``` @@ -98,20 +99,20 @@ sudo ./install.sh install 脚本默认拉取 GitHub 最新稳定 Release 对应的精确 GHCR tag;该 tag 是 `linux/amd64` + `linux/arm64` 多架构镜像,Docker 会自动匹配运行架构。脚本生成权限为 `0600` 的 `.env`,使用 `agentsql-data` 命名卷,并固定绑定回环地址: ```bash -curl -fsSL https://raw.githubusercontent.com/cuipengdba/agentsql/v1.9.0/scripts/quickstart.sh -o quickstart.sh && sh quickstart.sh --version v1.9.0 +curl -fsSL https://raw.githubusercontent.com/cuipengdba/agentsql/v2.0.0/scripts/quickstart.sh -o quickstart.sh && sh quickstart.sh --version v2.0.0 ``` -GHCR 包必须由发布者设为 public,以上命令才能在未登录环境匿名拉取。先核对 `v1.9.0` 精确 tag 的公开状态与双架构 manifest;Docker 会从 manifest list 自动选择 `linux/amd64` 或 `linux/arm64`: +GHCR 包必须由发布者设为 public,以上命令才能在未登录环境匿名拉取。先核对 `v2.0.0` 精确 tag 的公开状态与双架构 manifest;Docker 会从 manifest list 自动选择 `linux/amd64` 或 `linux/arm64`: ```bash -docker pull ghcr.io/cuipengdba/agentsql:v1.9.0 +docker pull ghcr.io/cuipengdba/agentsql:v2.0.0 ``` 等价的单条 `docker run` 如下;随机值只通过当前 shell 环境传入,不写进命令行参数: ```bash export AGENTSQL_SECRET="$(openssl rand -base64 24)" AGENTSQL_ADMIN_USER=admin AGENTSQL_ADMIN_PASSWORD="$(openssl rand -base64 18)" -docker run -d --name agentsql --restart unless-stopped --security-opt no-new-privileges:true -p 127.0.0.1:7780:7780 -e AGENTSQL_SECRET -e AGENTSQL_ADMIN_USER -e AGENTSQL_ADMIN_PASSWORD -v agentsql-data:/var/lib/agentsql ghcr.io/cuipengdba/agentsql:v1.9.0 +docker run -d --name agentsql --restart unless-stopped --security-opt no-new-privileges:true -p 127.0.0.1:7780:7780 -e AGENTSQL_SECRET -e AGENTSQL_ADMIN_USER -e AGENTSQL_ADMIN_PASSWORD -v agentsql-data:/var/lib/agentsql ghcr.io/cuipengdba/agentsql:v2.0.0 ``` ### 3. 本地 Live Demo(一条命令,无需源码) @@ -120,7 +121,7 @@ docker run -d --name agentsql --restart unless-stopped --security-opt no-new-pri ```bash mkdir agentsql-demo && cd agentsql-demo -curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/cuipengdba/agentsql/v1.9.0/deploy/quickstart/docker-compose.yml +curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/cuipengdba/agentsql/v2.0.0/deploy/quickstart/docker-compose.yml docker compose up -d ``` @@ -163,7 +164,7 @@ docker compose --profile observability up -d --build 以下源码构建示例仅生成本机可执行文件。自行构建的文件没有经过正式发布资产的签名与分发验收: ```bash -make build VERSION=v1.9.0 +make build VERSION=v2.0.0 ./bin/agentsqlctl init-config -o config.yaml export AGENTSQL_SECRET="$(openssl rand -base64 24)" export AGENTSQL_ADMIN_USER='admin' @@ -268,7 +269,7 @@ v0.5 审计报表支持 CSV、JSONL、纯 Go 生成的 PDF,以及包含四类 ## 数据库兼容矩阵 -v1.9.0 代码的注册表覆盖 **8 大类、29 款数据库**(8 款关系型 + 21 款 NoSQL / 向量)。瀚高 HighGo 已作为独立的限定只读类型计入;金仓 KingbaseES V9 另列为指定实例 PG 协议路径,不计入 29 款;分类表共 30 行。🟢 表示解析、授权、受控执行、脱敏、审计链路在所述范围通过;🔵 受控只读=连接、元数据、只读查询子集;🔷 连接级=ping、版本、Schema、只读预览;🟡 待验证=等厂商环境。指定实例闭环不代表完整生产支持。 +v2.0.0 代码的注册表覆盖 **8 大类、29 款数据库**(8 款关系型 + 21 款 NoSQL / 向量)。瀚高 HighGo 已作为独立的限定只读类型计入;金仓 KingbaseES V9 另列为指定实例 PG 协议路径,不计入 29 款;分类表共 30 行。🟢 表示解析、授权、受控执行、脱敏、审计链路在所述范围通过;🔵 受控只读=连接、元数据、只读查询子集;🔷 连接级=ping、版本、Schema、只读预览;🟡 待验证=等厂商环境。指定实例闭环不代表完整生产支持。 | 类别 | 数据库 | 适配状态 | 边界说明 | | --- | --- | --- | --- | @@ -356,7 +357,7 @@ NoSQL / 向量目前是连接级、低阶支持;只读预览不代表完整查 ```bash go vet ./... go test -race -count=1 ./... -make build VERSION=v1.9.0 +make build VERSION=v2.0.0 ``` `pg_query_go` 要求 cgo;不要使用 `CGO_ENABLED=0` 或 Alpine/musl 构建。提交改动前请阅读 [SPEC](docs/SPEC.md),为行为变化补测试,并保持 `tests/corpus` 决策语料不被无意改写。 diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md new file mode 100644 index 00000000..545b3d02 --- /dev/null +++ b/deploy/kubernetes/README.md @@ -0,0 +1,74 @@ +# AgentSQL v2.0.0 双网关 Kubernetes 部署 + +此清单只针对**两个节点、固定两个副本、无状态 MCP 只读请求**。运行配置把 HTTP 状态会话、事件续传和 B5 事务关闭;这不是已有单节点 B5 能力的迁移方案。真实生产支持范围以[v2.0.0 设计与验收](../../docs/v2.0.0-design.md)和本版发布证据为准。 + +## 准备 + +- 两个可调度 Kubernetes 节点,支持每副本独立 `ReadWriteOnce` PVC 的 StorageClass。 +- 已配置 TLS、备份和最小权限运行账号的 PostgreSQL metadata 库及独立 audit 库。不要使用 SQLite。两个 DSN 均须指向同一套经确认的控制面数据,运行账号不得有 DDL 权限。 +- 私有环境文件分别供 `agentsql-runtime` 与 `agentsql-migration` Secret 创建。文件不进入 Git;迁移文件使用具备 DDL 权限的双库账号,运行文件使用最小权限账号。两者必须使用同一**恰好 32 字节**的 `AGENTSQL_SECRET`,并提供 `AGENTSQL_ADMIN_PASSWORD`、`AGENTSQL_STORE_METADATA_DSN`、`AGENTSQL_STORE_AUDIT_DSN`。凭据只通过 Secret 注入。 +- 迁移前对 metadata 与 audit 做成套备份,并记录回滚点。新部署不要与旧版同时写入同一控制面,除非混合版本验收已通过。 + +以下命令假设当前目录是仓库根目录,命名空间为 `agentsql`。环境文件路径由运维指定,不在命令行包含凭据内容。 + +```sh +kubectl create namespace agentsql +kubectl -n agentsql create secret generic agentsql-runtime --from-env-file=/secure/agentsql-runtime.env +kubectl -n agentsql create secret generic agentsql-migration --from-env-file=/secure/agentsql-migration.env +kubectl -n agentsql apply -f deploy/kubernetes/configmap.yaml +``` + +## 首次安装或从 v1.9.0 升级 + +先对两个 PostgreSQL 库做成套备份并记录旧镜像摘要。`migrate` 可能执行 DDL;即使本版 schema 未变,也要记录检查结果。Job 不在基础 `kustomization.yaml` 中,避免普通部署隐式迁移。**B2 对活跃网关的二进制摘要要求一致,v1.9.0 与 v2.0.0 不能混合提供服务;此部署使用 `OnDelete`,版本切换必须安排维护窗口。** + +从 v1.9.0 升级时,先执行下列维护窗口操作;首次安装跳过这一段。 + +```sh +kubectl -n agentsql scale statefulset/agentsql --replicas=0 +kubectl -n agentsql wait --for=delete pod/agentsql-0 pod/agentsql-1 --timeout=180s +# 等待旧运行时租约到期;默认租约 15 秒,实际以配置值为准,留出时钟偏差。 +sleep 30 +``` + +旧网关完全停止后执行迁移和只读 schema 检查。首次安装也从此处开始。 + +```sh +kubectl -n agentsql apply -f deploy/kubernetes/jobs/migrate.yaml +kubectl -n agentsql wait --for=condition=complete job/agentsql-migrate-v2-0-0 --timeout=300s +kubectl -n agentsql logs job/agentsql-migrate-v2-0-0 +kubectl -n agentsql apply -f deploy/kubernetes/jobs/schema-check.yaml +kubectl -n agentsql wait --for=condition=complete job/agentsql-schema-check-v2-0-0 --timeout=300s +kubectl -n agentsql logs job/agentsql-schema-check-v2-0-0 +bash deploy/kubernetes/preflight.sh agentsql +``` + +检查通过后应用并验收双副本。 + +```sh +kubectl -n agentsql apply -k deploy/kubernetes +kubectl -n agentsql wait --for=create pod/agentsql-0 pod/agentsql-1 --timeout=180s +kubectl -n agentsql wait --for=condition=Ready pod/agentsql-0 pod/agentsql-1 --timeout=600s +``` + +`migrate --check` 必须对 metadata 和 audit 均显示 `status=current`,退出码为 0。缺失版本表、落后或超前版本时不得启动网关。两个副本的 initContainer 会再次只读检查。就绪探针只把可用副本加入 Service;存活探针只判断进程存活。 + +新建 PostgreSQL 列级策略时,先创建同对象的表级允许策略,再创建 `object_type: column`、`action: allow`、明确 `columns` 清单的列策略。读取该列策略的 `ETag`,以 `If-Match` 调用 `POST /api/v1/policies/{id}/bind-postgres`,请求体为 `{"sql":"SELECT id,name,phone FROM public.customers"}`。登记 SQL 只用于目录证明,不执行;本版只接受 PostgreSQL `CATALOG_CLOSED_V1` 的单张普通表、明确列投影,且所有引用列必须在策略清单中。返回的新策略修订和正式绑定须经两节点 MCP 允许/拒绝复验。原生扩展模式、视图、跨表或通配投影的管理端登记仍不开放;失败时不会把未绑定列策略当作表授权执行。 + +部署验收应分别读取 `agentsql-0` 和 `agentsql-1` 的 `/healthz`、`/readyz`,确认不同 B2 实例 ID;通过 Service 各做允许、拒绝、脱敏及持久审计测试。再停止一个 Pod,确认另一 Pod 继续处理、丢失节点上的活跃请求按失败处理且不会静默续接。审计或 metadata 故障时请求须关闭,恢复后再复测。不要把 HTTP 200 作为完整安全验收。 + +如果 metadata 中断超过 B2 租约,现存进程会保持 `B2_RUNTIME_HEARTBEAT_FAILED` 或 `B2_RUNTIME_LEASE_EXPIRED` 降级;恢复数据库后按维护流程重建两个 Pod,并再次确认两者 `B2_READY`、允许/拒绝/脱敏和持久审计。仅数据库重新连通不能视为能力恢复。 + +```sh +kubectl -n agentsql get pods -l app.kubernetes.io/name=agentsql -o wide +kubectl -n agentsql get endpointslice -l kubernetes.io/service-name=agentsql +kubectl -n agentsql port-forward pod/agentsql-0 17780:7780 +``` + +两个副本各有独立 1 GiB PVC,用于持久 B2 实例标识。不得把同一 PVC 挂载给两个副本,不得在普通升级时删除 PVC。固定两副本每节点 Agent QPS 上限为 10,集群理论上界为 20;单节点故障时上界为 10,分布不均可能更早限流。改变副本数和预算前必须重新设计并验证。 + +## 变更、回滚和凭据 + +升级前运行只读 schema Job、成套备份、记录当前镜像摘要和配置。明确停止两个旧 Pod,等旧租约过期,再应用新镜像并启动两个新 Pod;迁移或版本偏差失败时停止发布,检查日志与数据库快照。代码回滚同样须先停止两个新版 Pod、等待租约过期,再使用旧版正式镜像和经过隔离验证的双库备份;若 schema 已变化,不能仅回退镜像。管理员入口应由外层 TLS/访问控制保护;本清单不创建公网 Ingress。 + +迁移完成后删除迁移 Job 与 `agentsql-migration` Secret。运行 Secret 轮换需在维护窗口先停止双副本,验证新密钥与现存加密数据和 B2 租约的兼容性,再同时启动新副本;不能直接更换 `AGENTSQL_SECRET` 而不迁移或验证加密数据。所有日志和验收报告仅记录版本、退出码、非敏感实例 ID 和审计 ID,不记录 DSN、密码或 Agent Key。 diff --git a/deploy/kubernetes/configmap.yaml b/deploy/kubernetes/configmap.yaml new file mode 100644 index 00000000..e3612e10 --- /dev/null +++ b/deploy/kubernetes/configmap.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: agentsql-gateway-config + labels: + app.kubernetes.io/name: agentsql +data: + config.yaml: | + server: + http_listen: "0.0.0.0:7780" + console_enabled: true + event_stream: false + store: + metadata: + driver: postgres + dsn: "" + max_open_conns: 10 + max_idle_conns: 5 + conn_max_lifetime: "30m" + audit: + separate: true + driver: postgres + dsn: "" + max_open_conns: 10 + max_idle_conns: 5 + auto_migrate: false + column_authorization: + enabled: true + dry_run: false + instance_id_file: "/var/lib/agentsql/instance-id" + mcp: + http: + stateful: false + event_store_enabled: false + sessions: + enabled: false + transactions: + postgres: false + mysql: false + defaults: + statement_timeout_ms: 5000 + row_limit: 1000 + max_conns_per_datasource: 5 + # Two fixed replicas: each enforces 10 QPS, so the cluster upper bound is 20. + qps_per_agent: 10 + theme: + default: "dark" diff --git a/deploy/kubernetes/jobs/migrate.yaml b/deploy/kubernetes/jobs/migrate.yaml new file mode 100644 index 00000000..e0728a98 --- /dev/null +++ b/deploy/kubernetes/jobs/migrate.yaml @@ -0,0 +1,54 @@ +# Apply this Job only during an operator-controlled maintenance window after +# taking a paired metadata/audit backup. It is intentionally not in the base +# kustomization. The agentsql-migration Secret uses DDL owner DSNs. +apiVersion: batch/v1 +kind: Job +metadata: + name: agentsql-migrate-v2-0-0 + labels: + app.kubernetes.io/name: agentsql +spec: + backoffLimit: 0 + template: + spec: + automountServiceAccountToken: false + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + seccompProfile: + type: RuntimeDefault + containers: + - name: migrate + image: ghcr.io/cuipengdba/agentsql:v2.0.0 + command: + - /usr/local/bin/agentsqlctl + - migrate + - --config + - /etc/agentsql/config.yaml + envFrom: + - secretRef: + name: agentsql-migration + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: config + mountPath: /etc/agentsql/config.yaml + subPath: config.yaml + readOnly: true + - name: state + mountPath: /var/lib/agentsql + - name: tmp + mountPath: /tmp + volumes: + - name: config + configMap: + name: agentsql-gateway-config + - name: state + emptyDir: {} + - name: tmp + emptyDir: {} diff --git a/deploy/kubernetes/jobs/schema-check.yaml b/deploy/kubernetes/jobs/schema-check.yaml new file mode 100644 index 00000000..f58e9b1b --- /dev/null +++ b/deploy/kubernetes/jobs/schema-check.yaml @@ -0,0 +1,54 @@ +# Read-only schema preflight. Run this before a version change. It does not +# execute DDL or use the migration owner's credentials. +apiVersion: batch/v1 +kind: Job +metadata: + name: agentsql-schema-check-v2-0-0 + labels: + app.kubernetes.io/name: agentsql +spec: + backoffLimit: 0 + template: + spec: + automountServiceAccountToken: false + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + seccompProfile: + type: RuntimeDefault + containers: + - name: schema-check + image: ghcr.io/cuipengdba/agentsql:v2.0.0 + command: + - /usr/local/bin/agentsqlctl + - migrate + - --check + - --config + - /etc/agentsql/config.yaml + envFrom: + - secretRef: + name: agentsql-runtime + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: config + mountPath: /etc/agentsql/config.yaml + subPath: config.yaml + readOnly: true + - name: state + mountPath: /var/lib/agentsql + - name: tmp + mountPath: /tmp + volumes: + - name: config + configMap: + name: agentsql-gateway-config + - name: state + emptyDir: {} + - name: tmp + emptyDir: {} diff --git a/deploy/kubernetes/kustomization.yaml b/deploy/kubernetes/kustomization.yaml new file mode 100644 index 00000000..267304cc --- /dev/null +++ b/deploy/kubernetes/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - configmap.yaml + - service.yaml + - statefulset.yaml + - pdb.yaml diff --git a/deploy/kubernetes/pdb.yaml b/deploy/kubernetes/pdb.yaml new file mode 100644 index 00000000..1a9c1520 --- /dev/null +++ b/deploy/kubernetes/pdb.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: agentsql + labels: + app.kubernetes.io/name: agentsql +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: agentsql diff --git a/deploy/kubernetes/preflight.sh b/deploy/kubernetes/preflight.sh new file mode 100644 index 00000000..99d35d11 --- /dev/null +++ b/deploy/kubernetes/preflight.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +namespace="${1:-default}" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +command -v kubectl >/dev/null || { echo 'kubectl is required' >&2; exit 1; } + +# A fixed two-replica StatefulSet has required anti-affinity by hostname. +# Count ready workers; control-plane nodes are normally tainted NoSchedule. +node_count="$(kubectl get nodes --no-headers | awk '$2 == "Ready" && $3 !~ /control-plane|master/ { count++ } END { print count+0 }')" +if [[ "$node_count" -lt 2 ]]; then + echo 'preflight failed: at least two Ready worker nodes are required' >&2 + exit 1 +fi +kubectl wait --for=condition=Ready node --all --timeout=30s >/dev/null + +for key in AGENTSQL_SECRET AGENTSQL_ADMIN_PASSWORD AGENTSQL_STORE_METADATA_DSN AGENTSQL_STORE_AUDIT_DSN; do + if [[ -z "$(kubectl -n "$namespace" get secret agentsql-runtime -o "jsonpath={.data.${key}}" 2>/dev/null)" ]]; then + echo "preflight failed: agentsql-runtime is missing $key" >&2 + exit 1 + fi +done + +rendered="$(kubectl kustomize "$script_dir")" +if ! grep -Fq 'replicas: 2' <<<"$rendered" || + ! grep -Fq 'type: OnDelete' <<<"$rendered" || + ! grep -Fq 'image: ghcr.io/cuipengdba/agentsql:v2.0.0' <<<"$rendered" || + ! grep -Fq 'instance_id_file: "/var/lib/agentsql/instance-id"' <<<"$rendered" || + [[ "$(grep -Fc 'driver: postgres' <<<"$rendered")" -lt 2 ]] || + grep -Fq 'sqlite_path:' <<<"$rendered" || + ! grep -Fq 'stateful: false' <<<"$rendered" || + ! grep -Fq 'event_store_enabled: false' <<<"$rendered" || + ! grep -Fq 'auto_migrate: false' <<<"$rendered"; then + echo 'preflight failed: expected fixed two-replica v2.0.0 safety profile is missing' >&2 + exit 1 +fi + +kubectl -n "$namespace" apply --dry-run=server -k "$script_dir" >/dev/null +echo "KUBERNETES_PREFLIGHT=PASS namespace=$namespace nodes=$node_count" diff --git a/deploy/kubernetes/service.yaml b/deploy/kubernetes/service.yaml new file mode 100644 index 00000000..fd5f1f8c --- /dev/null +++ b/deploy/kubernetes/service.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Service +metadata: + name: agentsql + labels: + app.kubernetes.io/name: agentsql +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: agentsql + ports: + - name: http + port: 7780 + targetPort: http +--- +apiVersion: v1 +kind: Service +metadata: + name: agentsql-headless + labels: + app.kubernetes.io/name: agentsql +spec: + clusterIP: None + selector: + app.kubernetes.io/name: agentsql + ports: + - name: http + port: 7780 + targetPort: http diff --git a/deploy/kubernetes/statefulset.yaml b/deploy/kubernetes/statefulset.yaml new file mode 100644 index 00000000..1330b7b2 --- /dev/null +++ b/deploy/kubernetes/statefulset.yaml @@ -0,0 +1,135 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: agentsql + labels: + app.kubernetes.io/name: agentsql +spec: + serviceName: agentsql-headless + replicas: 2 + minReadySeconds: 10 + podManagementPolicy: Parallel + selector: + matchLabels: + app.kubernetes.io/name: agentsql + updateStrategy: + # B2 rejects two simultaneously active binary digests. Operators must + # drain both replicas and wait for their leases to expire before upgrade. + type: OnDelete + template: + metadata: + labels: + app.kubernetes.io/name: agentsql + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 45 + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + seccompProfile: + type: RuntimeDefault + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: agentsql + topologyKey: kubernetes.io/hostname + initContainers: + - name: schema-check + image: ghcr.io/cuipengdba/agentsql:v2.0.0 + imagePullPolicy: IfNotPresent + command: + - /usr/local/bin/agentsqlctl + - migrate + - --check + - --config + - /etc/agentsql/config.yaml + envFrom: + - secretRef: + name: agentsql-runtime + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: config + mountPath: /etc/agentsql/config.yaml + subPath: config.yaml + readOnly: true + - name: state + mountPath: /var/lib/agentsql + - name: tmp + mountPath: /tmp + containers: + - name: gateway + image: ghcr.io/cuipengdba/agentsql:v2.0.0 + imagePullPolicy: IfNotPresent + args: + - serve + - --config + - /etc/agentsql/config.yaml + ports: + - name: http + containerPort: 7780 + envFrom: + - secretRef: + name: agentsql-runtime + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + startupProbe: + httpGet: + path: /readyz + port: http + periodSeconds: 10 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /readyz + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 2 + livenessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 15 + timeoutSeconds: 3 + failureThreshold: 3 + volumeMounts: + - name: config + mountPath: /etc/agentsql/config.yaml + subPath: config.yaml + readOnly: true + - name: state + mountPath: /var/lib/agentsql + - name: tmp + mountPath: /tmp + volumes: + - name: config + configMap: + name: agentsql-gateway-config + - name: tmp + emptyDir: {} + volumeClaimTemplates: + - metadata: + name: state + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi diff --git a/deploy/quickstart/docker-compose.debug.yml b/deploy/quickstart/docker-compose.debug.yml index 9197ceac..dab1a3f5 100644 --- a/deploy/quickstart/docker-compose.debug.yml +++ b/deploy/quickstart/docker-compose.debug.yml @@ -1,6 +1,6 @@ services: seed: - image: ghcr.io/cuipengdba/agentsql:v1.6.0-quickstart + image: ghcr.io/cuipengdba/agentsql:v2.0.0-quickstart gateway: image: agentsql-quickstart-gateway:debug volumes: diff --git a/deploy/quickstart/docker-compose.yml b/deploy/quickstart/docker-compose.yml index 01368bc3..02cd6de5 100644 --- a/deploy/quickstart/docker-compose.yml +++ b/deploy/quickstart/docker-compose.yml @@ -49,7 +49,7 @@ services: - demo-mysql seed: - image: ghcr.io/cuipengdba/agentsql:v1.6.0-quickstart + image: ghcr.io/cuipengdba/agentsql:v2.0.0-quickstart container_name: agentsql-quickstart-seed restart: "no" entrypoint: ["/usr/local/bin/agentsqlctl"] @@ -77,7 +77,7 @@ services: condition: service_healthy gateway: - image: ghcr.io/cuipengdba/agentsql:v1.6.0-quickstart + image: ghcr.io/cuipengdba/agentsql:v2.0.0-quickstart container_name: agentsql-quickstart-gateway restart: unless-stopped environment: diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 4009bd21..18df2c2a 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -2,7 +2,7 @@ AgentSQL 是面向 AI Agent 的数据库安全网关和生产级 MCP Server。元数据与审计记录默认保存在本地 SQLite,也可使用 PostgreSQL 控制面;业务数据库仍使用各自的 PostgreSQL 或 MySQL 连接。 -当前精确安装版本、下载链接和升级前检查见 [v1.6.0 快速上手](GETTING_STARTED.md)与 [v1.6.0 发布说明](release-notes-v1.6.0.md)。本文后部标有 v1.2.0 或其他旧版本号的发布命令与分阶段上线记录是历史示例,不应直接用于 v1.6.0;执行前须使用正式 Release 的同版资产、签名及镜像摘要。 +当前精确安装版本、下载链接和升级前检查见 [快速上手](GETTING_STARTED.md)与 [v2.0.0 发布说明](release-notes-v2.0.0.md)。固定双网关 Kubernetes 部署另见 [v2.0.0 操作指南](../deploy/kubernetes/README.md)。本文后部标有 v1.2.0 或其他旧版本号的发布命令与分阶段上线记录是历史示例;执行前须使用正式 Release 的同版资产、签名及镜像摘要。 ## 目录约定 diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index b390b548..5882aee1 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -1,6 +1,6 @@ # AgentSQL 快速上手 -> 本文说明 AgentSQL v1.9.0 的安装与使用。执行下列固定 v1.9.0 tag 的安装、Demo 和镜像命令前,先在 [GitHub Releases](https://github.com/cuipengdba/agentsql/releases) 核对正式资产及签名,并确认对应 GHCR 镜像可以匿名拉取;源码仓库为 `github.com/cuipengdba/agentsql`。 +> 本文说明 AgentSQL v2.0.0 的安装与使用。执行下列固定 v2.0.0 tag 的安装、Demo 和镜像命令前,先在 [GitHub Releases](https://github.com/cuipengdba/agentsql/releases) 核对正式资产及签名,并确认对应 GHCR 镜像可以匿名拉取;源码仓库为 `github.com/cuipengdba/agentsql`。 AgentSQL 是放在 AI Agent 与业务数据库之间的数据库安全网关和生产级 MCP Server,让每条 SQL 在执行前经过身份、权限、规则和决策检查,并在可审计的边界内执行。 @@ -31,15 +31,15 @@ AgentSQL 不是 BI、ORM 或 Text2SQL,不负责把自然语言转换成 SQL, | 范围 | 当前支持 | 说明 | | --- | --- | --- | | 一键安装宿主 | `linux/amd64`、`linux/arm64`;glibc 2.28+、systemd | v0.4.0 起提供 linux/arm64 原生 glibc 包;`x86_64` / `amd64` 对应 amd64,`aarch64` / `arm64` 对应 arm64 | -| 其他宿主 | Docker / Docker Compose | v1.9.0 GHCR tag 须经 `linux/amd64` + `linux/arm64` multi-arch manifest 与匿名拉取核验;musl / Alpine、CentOS 7 与上述两种架构之外的平台不提供原生包 | +| 其他宿主 | Docker / Docker Compose | v2.0.0 GHCR tag 须经 `linux/amd64` + `linux/arm64` multi-arch manifest 与匿名拉取核验;musl / Alpine、CentOS 7 与上述两种架构之外的平台不提供原生包 | | 业务数据库 | MySQL 8;PostgreSQL 14–18 | 为 AgentSQL 创建独立、最小权限运行账号 | | 控制面存储 | SQLite;PostgreSQL 15+ | SQLite 为默认;PostgreSQL 可分离 metadata 与 audit | | 浏览器控制台 | `http://127.0.0.1:7780` | 默认绑定回环地址 | -| 本地 Live Demo | Docker Engine / Docker Desktop、Docker Compose v2 | 使用 v1.9.0 精确 tag 的 Compose 和经核验的 GHCR 镜像;无需克隆源码或构建 | +| 本地 Live Demo | Docker Engine / Docker Desktop、Docker Compose v2 | 使用 v2.0.0 精确 tag 的 Compose 和经核验的 GHCR 镜像;无需克隆源码或构建 | ## 路径 A:5 分钟零配置看效果(一条命令) -这是 v1.9.0 专用 Demo 模式:使用合成数据和固定演示身份,无需克隆源码、无需编辑任何配置、无需构建镜像。执行前须核对 v1.9.0 Git tag 中的 Compose 文件与其引用的三个 GHCR 镜像均已公开且可匿名拉取;首次启动会拉取镜像并播种固定演示数据。 +这是 v2.0.0 专用 Demo 模式:使用合成数据和固定演示身份,无需克隆源码、无需编辑任何配置、无需构建镜像。执行前须核对 v2.0.0 Git tag 中的 Compose 文件与其引用的三个 GHCR 镜像均已公开且可匿名拉取;首次启动会拉取镜像并播种固定演示数据。 只需一个 `docker-compose.yml`(在任意空目录执行): @@ -48,7 +48,7 @@ AgentSQL 不是 BI、ORM 或 Text2SQL,不负责把自然语言转换成 SQL, mkdir agentsql-demo && cd agentsql-demo # 2) 下载自包含 compose(仅这一个文件,约 2KB) -curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/cuipengdba/agentsql/v1.9.0/deploy/quickstart/docker-compose.yml +curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/cuipengdba/agentsql/v2.0.0/deploy/quickstart/docker-compose.yml # 3) 一条命令拉起全部容器(postgres + mysql + seed + gateway) docker compose up -d @@ -58,7 +58,7 @@ Windows(PowerShell)用等价命令: ```powershell New-Item -ItemType Directory -Force agentsql-demo | Out-Null; Set-Location agentsql-demo -Invoke-WebRequest -OutFile docker-compose.yml https://raw.githubusercontent.com/cuipengdba/agentsql/v1.9.0/deploy/quickstart/docker-compose.yml +Invoke-WebRequest -OutFile docker-compose.yml https://raw.githubusercontent.com/cuipengdba/agentsql/v2.0.0/deploy/quickstart/docker-compose.yml docker compose up -d ``` @@ -93,16 +93,16 @@ docker compose down -v **v0.4.0 起提供 linux/arm64 原生 glibc 包:**一键安装支持 `linux/amd64`(`uname -m` 为 `x86_64` / `amd64`)和 `linux/arm64`(`aarch64` / `arm64`),均要求 glibc 2.28+、systemd,且命令需要 root 权限: -以下示例固定 v1.9.0。执行前先确认正式 Release 提供该版本的 `install.sh`、`SHA256SUMS` 和签名;按 [发布资产说明](RELEASE_ASSETS.md) 使用独立核验的公钥验签清单,再用清单中的哈希核验安装器。 +以下示例固定 v2.0.0。执行前先确认正式 Release 提供该版本的 `install.sh`、`SHA256SUMS` 和签名;按 [发布资产说明](RELEASE_ASSETS.md) 使用独立核验的公钥验签清单,再用清单中的哈希核验安装器。 ```bash # 适用前提:linux/amd64 或 linux/arm64 + glibc 2.28+ + systemd -curl -fsSLo install.sh https://github.com/cuipengdba/agentsql/releases/download/v1.9.0/install.sh +curl -fsSLo install.sh https://github.com/cuipengdba/agentsql/releases/download/v2.0.0/install.sh # 先用已验签的 SHA256SUMS 核验 install.sh,再运行: -sudo sh ./install.sh install --version v1.9.0 +sudo sh ./install.sh install --version v2.0.0 ``` -除一键安装外,也可按 [部署指南](DEPLOY.md) 使用源码或 Docker Compose 构建。确认 v1.9.0 镜像已公开、双架构 manifest 和匿名拉取均通过后,运行 `docker pull ghcr.io/cuipengdba/agentsql:v1.9.0`;manifest 应匹配 `linux/amd64` / `linux/arm64`。发布状态和资产以 GitHub Releases 页面为准。 +除一键安装外,也可按 [部署指南](DEPLOY.md) 使用源码或 Docker Compose 构建。确认 v2.0.0 镜像已公开、双架构 manifest 和匿名拉取均通过后,运行 `docker pull ghcr.io/cuipengdba/agentsql:v2.0.0`;manifest 应匹配 `linux/amd64` / `linux/arm64`。发布状态和资产以 GitHub Releases 页面为准。 ### 2. 登录控制台 @@ -130,7 +130,7 @@ PostgreSQL 与 Vastbase G100 Build 9 PG 模式的指定实例可在数据源表 在「权限」中新建绑定该 Agent 与数据源的 `allow` 策略,从确实需要的单张表开始,例如 `public.customers` 或 `customers`。不要用 `*` 或 `schema.*` 做普通示例:两者虽然合法,却代表整表全列授权,会让列级白名单失效。 -安装 v1.9.0 后,控制台「接入向导」可复用同一个 Agent 和数据源选择做连接与权限预检、目标 SQL 离线解释,并生成 Dify 1.17.1 或 MCP Inspector 0.16.8 的无密钥接入说明。预检不执行 Agent SQL;策略数量大于零或离线结论为“未知”都不证明目标 SQL 已获授权。请在真实客户端分别验证允许与拒绝请求,再用响应中的审计 ID 跳转到审计详情核对。配置步骤与客户端可达地址的限制见[使用手册](USER_GUIDE.md#接入向导-onboardingv190)。 +安装 v2.0.0 后,控制台「接入向导」可复用同一个 Agent 和数据源选择做连接与权限预检、目标 SQL 离线解释,并生成 Dify 1.17.1 或 MCP Inspector 0.16.8 的无密钥接入说明。预检不执行 Agent SQL;策略数量大于零或离线结论为“未知”都不证明目标 SQL 已获授权。请在真实客户端分别验证允许与拒绝请求,再用响应中的审计 ID 跳转到审计详情核对。配置步骤与客户端可达地址的限制见[使用手册](USER_GUIDE.md#接入向导-onboardingv190)。 ### 6. 接入 MCP 客户端 @@ -185,9 +185,9 @@ curl -fsS http://127.0.0.1:7780/healthz curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:7780/readyz ``` -## v1.9.0:升级前只读检查 +## v2.0.0:升级前只读检查 -正式发布并取得 v1.9.0 `agentsqlctl` 后,使用目标环境的配置先检查控制面 schema 版本。v1.9.0 不增加 schema 迁移;从 v1.8.0 升级仍须成套备份、只读检查并在隔离环境演练恢复: +正式发布并取得 v2.0.0 `agentsqlctl` 后,使用目标环境的配置先检查控制面 schema 版本。v2.0.0 不增加 schema 迁移;从 v1.9.0 升级仍须成套备份、只读检查并在隔离环境演练恢复。双网关 Kubernetes 部署另须按[维护窗口操作指南](../deploy/kubernetes/README.md)停止旧版双副本、等待 B2 租约到期后再启动新版,不支持新旧二进制同时活跃: ```bash agentsqlctl migrate --check --config /etc/agentsql/config.yaml diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index f96c9b26..96f135e5 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -1,6 +1,6 @@ # AgentSQL MCP 接入指南 -> 本文说明 AgentSQL v1.9.0 的 MCP 接入。安装或拉取 v1.9.0 前,先在 [GitHub Releases](https://github.com/cuipengdba/agentsql/releases) 核对正式资产与签名,并确认对应 GHCR 镜像可以匿名拉取;源码仓库为 `github.com/cuipengdba/agentsql`。 +> 本文说明 AgentSQL v2.0.0 的 MCP 接入。安装或拉取 v2.0.0 前,先在 [GitHub Releases](https://github.com/cuipengdba/agentsql/releases) 核对正式资产与签名,并确认对应 GHCR 镜像可以匿名拉取;源码仓库为 `github.com/cuipengdba/agentsql`。 本文说明如何把 MCP 客户端接到 AgentSQL。身份、授权、规则、审批与能力边界见 [使用手册](USER_GUIDE.md),首次部署见 [快速上手](GETTING_STARTED.md);把决策事件外发到 Webhook、告警群或 Syslog 见 [通知外发指南](NOTIFICATIONS.md)。 diff --git a/docs/RELEASE_ASSETS.md b/docs/RELEASE_ASSETS.md index 02e5fb59..42763c47 100644 --- a/docs/RELEASE_ASSETS.md +++ b/docs/RELEASE_ASSETS.md @@ -1,5 +1,9 @@ # AgentSQL Release 资产清单 +## v2.0.0 预期资产 + +`scripts/release-dryrun.ps1` 定义正式版本恰好 15 项资产:Linux amd64/arm64 安装包与各自 SHA256 文件、SPDX SBOM 与签名、provenance 与签名、SHA256SUMS 与签名、公钥、安装器、Go 版本元数据、SBOM 生成说明和验签指南。全部资产必须来自同一个干净封版提交。上传后应从 GitHub 匿名回下载,核对字节数、SHA256、Ed25519 签名、源提交及双架构实际内容;v2.0.0 的结果记录在[正式发布证据](v2.0.0-release-evidence.md)。 + ## v1.2.0 预期资产 15 项正式资产须来自 v1.2.0 同一干净封板提交: diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 9f4fab17..939367f5 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -1,8 +1,8 @@ # AgentSQL 使用手册 -> 本文说明 AgentSQL v1.9.0 的功能与操作。安装或拉取 v1.9.0 前,先在 [GitHub Releases](https://github.com/cuipengdba/agentsql/releases) 核对正式资产与签名,并确认对应 GHCR 镜像可以匿名拉取;源码仓库为 `github.com/cuipengdba/agentsql`。 +> 本文说明 AgentSQL v2.0.0 的功能与操作。安装或拉取 v2.0.0 前,先在 [GitHub Releases](https://github.com/cuipengdba/agentsql/releases) 核对正式资产与签名,并确认对应 GHCR 镜像可以匿名拉取;源码仓库为 `github.com/cuipengdba/agentsql`。 -本手册按控制台主要菜单说明 v1.9.0 的能力和操作。首次使用请先完成 [快速上手](GETTING_STARTED.md);MCP 客户端配置见 [接入指南](INTEGRATIONS.md);版本范围与验收状态见 [v1.9.0 发布说明](release-notes-v1.9.0.md)和[验收记录](v1.9.0-acceptance.md);「设置与集成 → 通知设置」的 Webhook、Syslog 与安全边界见 [通知外发指南](NOTIFICATIONS.md)。 +本手册按控制台主要菜单说明 v2.0.0 的能力和操作。首次使用请先完成 [快速上手](GETTING_STARTED.md);MCP 客户端配置见 [接入指南](INTEGRATIONS.md);版本范围与验收状态见 [v2.0.0 发布说明](release-notes-v2.0.0.md)和[验收记录](v2.0.0-acceptance.md);「设置与集成 → 通知设置」的 Webhook、Syslog 与安全边界见 [通知外发指南](NOTIFICATIONS.md)。 ## 1. 总览 `/` @@ -35,6 +35,12 @@ v1.9.0 在同一页面增加 Dify 1.17.1 / MCP Inspector 0.16.8 的接入说明 离线解释只返回“明确拒绝”或“尚不能确认允许”,并可列出对象、匹配策略 ID 与静态规则;它不连接业务库,也不执行 SQL。最后在真实客户端分别发起允许与拒绝请求,将响应的正整数审计 ID 填入向导直达审计详情,核对 Agent、数据源、决策、规则及脱敏。没有审计 ID 时按错误码先查认证、协议或依赖故障。向导不能替代逐对象授权、执行链路或生产支持验收。 +## v2.0.0:固定双网关与 PostgreSQL 列策略登记 + +双网关 Kubernetes 部署只承载无状态 MCP 只读请求。两个固定副本共享 PostgreSQL metadata 和独立 audit 库,每个副本使用自己的 B2 实例标识;B5 跨请求事务、HTTP 状态会话与事件续传在此配置中关闭。运维人员须按[双网关操作指南](../deploy/kubernetes/README.md)执行迁移检查、维护窗口切换、故障演练与回退。公开 Live Demo 为单网关合成数据环境。 + +具有策略管理权限的管理员可先创建 PostgreSQL 单张普通表的 `object_type: column`、`action: allow` 策略,使用该策略当前 `ETag` 调用 `POST /api/v1/policies/{id}/bind-postgres`,请求体为一条投影精确列清单的 `SELECT`。登记过程只分析 SQL 和数据库目录,不执行该 SELECT;成功后须在真实 MCP 客户端验证允许、拒绝、脱敏及持久审计。对象不符、额外引用列、多语句、视图、跨表、通配投影或原生扩展模式将拒绝;旧 `ETag` 返回 412。此 API 只覆盖本版闭合绑定模式的 PostgreSQL 单表范围。 + ## 2. 审计 `/audit` 审计页记录经过应用流水线的 SQL、对象、规则命中、决策、估算行数、返回行数、耗时与错误信息。它用于工程排障和行为追溯,不是法规级 WORM。 @@ -763,7 +769,7 @@ SQLite → PostgreSQL 控制面迁移要点:停止旧服务写入;备份 SQL - PostgreSQL 的 B2 列级授权、B5 跨请求逻辑会话与计划事务均出厂默认开启;MySQL 不进入 B2 PostgreSQL 路径,且不支持 B5 跨请求事务。每个 operation 仍只允许一条顶层 SQL,并受预检计划和会话安全边界约束。 - 完整原生路径仍以 PostgreSQL 14–18 与 MySQL 8 为主。DM、Oracle、YashanDB 的独立最小 dialect,以及 OpenTenBase、PolarDB 等协议兼容路径均受严格边界约束;达梦 DM8 Pack3、金仓 V9R1C10 PG 模式、Vastbase G100 Build 9 PG 模式在 v1.5.0 完成指定实例的真实客户端、权限撤销和断库恢复复验,v1.6.0 在相同范围内通过独立审计库故障、真实锁等待超时、客户端取消、数据库端终止和恢复后脱敏复验。v1.7.0 的独立 HighGo 类型仅限 9.0.10 企业版 PG 模式有界 SELECT;金仓 TLS 仅限 V9R1C10 PG 模式指定实例。范围见[瀚高验收](v1.7.0-acceptance.md)与[金仓 TLS 验收](v1.7.0-dm-kingbase-depth-acceptance.md),不得视为完整方言支持或厂商认证。 - v1.0.0 的 `/mcp/2026-07-28` 是独立无状态 HTTP 入口:每次 POST 都重新验证 Agent Key、由 Key 确定租户并与旧入口共用 Agent QPS;版本 Header、`Mcp-Method`、适用时的 `Mcp-Name` 必须与请求体匹配。旧 `/mcp` 与 stdio 继续服务旧协议;新入口没有 B5 跨请求逻辑会话或计划事务工具。客户端配置和请求示例见 [MCP 接入指南](INTEGRATIONS.md)。 -- v0.4.0 起提供 linux/arm64 原生 glibc 包(`aarch64` / `arm64`),并继续提供 linux/amd64 包(`x86_64` / `amd64`);v1.9.0 GHCR tag 须在公开发布并通过双架构 manifest 与匿名拉取验收后使用。v0.5.0 已提供可选的 TOTP MFA、OIDC 和 LDAP/AD 人员认证;配置与限制见 [身份认证指南](AUTHENTICATION.md)。法规级 WORM、SIEM、HA、Kubernetes、musl / Alpine 或 CentOS 7 原生一键安装仍不支持,RBAC / 多租户能力仍是 MVP。 +- v0.4.0 起提供 linux/arm64 原生 glibc 包(`aarch64` / `arm64`),并继续提供 linux/amd64 包(`x86_64` / `amd64`);v2.0.0 GHCR tag 须在公开发布并通过双架构 manifest 与匿名拉取验收后使用。v0.5.0 已提供可选的 TOTP MFA、OIDC 和 LDAP/AD 人员认证;配置与限制见 [身份认证指南](AUTHENTICATION.md)。v2.0.0 的 Kubernetes 清单限固定双网关与指定无状态只读路径;PostgreSQL 自身 HA、法规级 WORM、SIEM、musl / Alpine 或 CentOS 7 原生一键安装仍不支持,RBAC / 多租户能力仍是 MVP。 ## 在线安装包升级预演(与 schema 检查不同) 准备可信渠道提供的 HTTPS 清单,JSON 字段为 `version`、同源 HTTPS `url` 和下载内容的 `sha256`。当前仅支持版本检查和只读预演: diff --git a/docs/release-notes-v2.0.0.md b/docs/release-notes-v2.0.0.md new file mode 100644 index 00000000..fe3766df --- /dev/null +++ b/docs/release-notes-v2.0.0.md @@ -0,0 +1,17 @@ +# AgentSQL v2.0.0 发布说明 + +v2.0.0 增加固定双网关 Kubernetes 部署方案。两个网关共享 PostgreSQL 元数据库与独立审计库,各自保存 B2 运行时实例标识;无状态 MCP 只读请求在一个网关不可用时由另一个网关继续受控处理。部署包含非 root 运行、独立 PVC、节点反亲和、Service、就绪探针、手动迁移 Job、只读 schema 检查和部署前检查脚本。 + +PostgreSQL 列策略新增管理端 `POST /api/v1/policies/{id}/bind-postgres`。管理员提交策略当前 `ETag` 和一条登记用 SELECT;网关先发现目录,再在控制面策略版本锁下持有业务表目录锁,复核对象与列身份后原子提交正式绑定。登记 SQL 不执行。本版仅支持 `CATALOG_CLOSED_V1`、单张普通表和明确列投影;原生扩展模式、跨表、视图及通配投影的管理端登记仍会拒绝。 + +两个网关同一版本同时启动时,RBAC 初始创建与 B2 控制栅栏的短暂并发冲突会有有界重试。持续的数据库故障、策略未登记或不同二进制摘要仍保持失败关闭。 + +## 升级和运行边界 + +- 与 v1.9.0 使用相同的 PostgreSQL metadata 21、audit 6 schema。迁移仍须显式运行并检查双库状态,不能由网关暗中迁移。 +- B2 租约绑定二进制摘要,v1.9.0 与 v2.0.0 不能混合活跃。Kubernetes 清单使用 `OnDelete`;升级和回退均需维护窗口:成套备份、停止两个旧 Pod、等待租约过期、检查 schema,再启动新版本。**本版不承诺无停机滚动升级。** +- 双网关配置关闭 B5 跨请求事务、HTTP 状态会话与事件续传;不会把节点本地连接或 WAL 当作共享状态。原有单节点部署合同另行适用。 +- 固定两个副本,各自 Agent QPS 上限为 10,集群理论上界为 20;单节点故障时上界为 10。此限流按进程执行,不构成全局强一致配额;不支持任意水平扩容。 +- PostgreSQL 元数据库、审计库的高可用、TLS、备份、最小权限和容量由部署者提供。本版 Kubernetes 验收使用隔离的两节点 kind 集群与合成 PostgreSQL 数据,不等同于客户生产拓扑认证。 + +本版不增加数据库适配或扩大既有数据库的生产支持声明。部署、故障恢复与验收细节见 [Kubernetes 操作指南](../deploy/kubernetes/README.md)、[设计](v2.0.0-design.md)与[验收记录](v2.0.0-acceptance.md)。 diff --git a/docs/v2.0.0-acceptance.md b/docs/v2.0.0-acceptance.md new file mode 100644 index 00000000..2b05c7aa --- /dev/null +++ b/docs/v2.0.0-acceptance.md @@ -0,0 +1,21 @@ +# AgentSQL v2.0.0 候选环境验收 + +状态:功能候选已完成双节点演练;正式封版 CI、签名发行资产、GHCR、官网与公网 Live Demo 以独立发布证据记录为准。本文件不代表 v2.0.0 已发布。 + +测试环境:2026-10-18 至 2026-10-19,Windows 宿主机上的 kind v0.33.0,Kubernetes v1.37.0,1 个控制节点、2 个 Ready 工作节点。metadata、audit 和只读业务库均为隔离的 PostgreSQL 18 容器;业务库只有合成 `public.customers` 和未授权 `public.internal_notes`。双网关使用独立 RWO PVC、同一 32 字节密钥、PostgreSQL 分库存储和最小权限运行账号。测试网络未配置 TLS,也未模拟 PostgreSQL 自身主备故障。 + +| 验收项 | 候选环境实测 | +| --- | --- | +| 基线、迁移和备份 | 从正式 `v1.9.0` 镜像启动双节点;metadata 21、audit 6。成套 `pg_dump -Fc` 备份在两套独立 PostgreSQL 实例中恢复,v1.9.0 与 v2.0.0 候选二进制分别执行双库 `migrate --check`,均为 `current`。v2.0.0 独立迁移 Job 与只读 schema Job 通过。 | +| 双节点配置 | 部署前检查返回 `KUBERNETES_PREFLIGHT=PASS namespace=agentsql nodes=2`。两 Pod 分布于不同工作节点,独立 PVC 和不同 B2 实例 ID;同版二进制摘要一致,B2 协议 3 active。 | +| 并发启动 | v1.9.0 首次双 Pod 初始化出现 RBAC 唯一键竞态;候选版加有界重试。数据库恢复后同时重启还发现 B2 控制栅栏 ETag 竞态;候选版只对该启动冲突重读并有界重试。修复后两 Pod 同时就绪,持续不兼容仍保持降级。 | +| 管理端 PostgreSQL 列绑定 | 候选版通过 API 新建列策略,再调用 `bind-postgres`。错误对象和多语句 SQL 均返回 409;正确的单表闭合绑定模式登记 3 列、6 条 output/reference 用途权限,策略修订为 2、绑定状态 `healthy`;旧 ETag 返回 412。真实 SQL 执行只由后续 MCP 请求触发。登记后重建一个 Pod,B2 仍为 active,允许/拒绝与审计 ID 54/55 通过。 | +| 双节点 MCP 安全链路 | 候选版两节点各自执行允许查询、未授权表 R010 拒绝和手机号脱敏,审计库持久记录 ID 49–52:`allow,deny,allow,deny`,每次允许响应脱敏 2 个单元格。无状态 MCP 响应不发会话 ID。 | +| 真实 MCP 客户端 | MCP Inspector 0.16.8 与 Dify MCPClient 1.17.1 分别经候选网关执行单行允许查询、手机号脱敏与未授权表 R010 拒绝。独立审计库记录 ID 88–91:`allow,deny,allow,deny`。客户端运行在隔离宿主与容器中,通过临时 Host 头映射访问 kind Pod 转发端口;未把该映射写入生产配置。 | +| 合成短时容量与审计一致性 | 两节点各接收 8 条并发度 4 的允许查询,16/16 返回 `allow`、每条脱敏 2 个单元格,合计 1.88 秒、观察吞吐 8.53 QPS、p50 437.0 ms、p95 547.0 ms。响应的审计 ID 72–87 全部唯一;独立 audit 库查得 16/16 唯一记录,决策均为 `allow`。原始记录保存在验收主机 `D:\CodexLS\outputs\agentsql-v200\capacity-*.log`。这是单次短时合成观测,不是持续负载或生产容量承诺。 | +| 单节点故障 | cordon 一个工作节点并停止其 Pod 后,EndpointSlice 仅保留另一个 Ready Pod;经 Service 的允许/R010 拒绝、脱敏及审计继续通过(ID 19/20)。恢复节点后原 Pod 用原 PVC 重新就绪。 | +| 依赖故障关闭 | 停止独立 audit 库时受控查询返回 `error` 且不交付结果;恢复后再次允许/拒绝并写入审计 ID 22/23。停止 metadata 库时初始化返回 HTTP 401,B2 租约降级;恢复库后重建两个 Pod,双节点重新就绪。 | +| 身份与租户 | 轮换测试 Agent Key 后,旧 Key 在两节点均 HTTP 401;新 Key 在两节点均通过安全链路(审计 ID 43–46)。另一个租户的管理员读取原租户数据源返回 404,其 Agent 借用原数据源的 MCP 查询未得到数据。 | +| 升级和回退 | 由于 B2 严格比较二进制摘要,混合 v1.9.0/v2.0.0 的滚动更新会挡住新 Pod;清单已改 `OnDelete` 并采用维护窗口停双副本、等待租约到期。正式 v1.9.0 镜像回退后两 Pod 均通过安全链路(审计 ID 35–38),再切回 v2.0.0 候选并通过(ID 41/42)。 | + +边界:固定两个网关和指定 PG18/kind 拓扑;业务库是合成数据,未取得客户真实并发负载、生产网络、外部 TLS 或 PostgreSQL HA 证据。B5 跨请求事务、状态会话和事件续传在此双网关配置关闭;QPS 是每节点预算,非全局强一致配额。PostgreSQL 原生扩展模式的管理端列登记仍不支持,登记 API 只覆盖闭合绑定模式的单张普通表。 diff --git a/docs/v2.0.0-design.md b/docs/v2.0.0-design.md new file mode 100644 index 00000000..b6530c13 --- /dev/null +++ b/docs/v2.0.0-design.md @@ -0,0 +1,46 @@ +# AgentSQL v2.0.0 双网关 Kubernetes 设计 + +状态:候选实现及隔离双节点验收完成;正式发布须再通过封版 CI、签名资产与公网验收。 + +## 用户任务与范围 + +目标是让企业运维人员在 Kubernetes 中部署两个 AgentSQL 网关,使无状态 MCP 只读请求在单个网关故障时继续经另一个网关受控处理。两个副本共享 PostgreSQL metadata 和独立 audit 数据库;迁移由运维明确执行,运行副本仅使用最小权限账号且 `auto_migrate:false`。v1.4.0 已验证 PostgreSQL 双进程共享登录预算与审计故障关闭,本版补齐部署、升级回滚及故障演练。 + +本版首个双网关配置限定为固定两个副本的无状态 MCP 请求。B5 会话事务的物理连接和 WAL 属于节点本地状态;在验证节点亲和路由、owner epoch 与 WAL 恢复前,双网关配置明确关闭 B5。HTTP MCP 状态会话与事件续传也在该配置关闭。单节点原有能力保持原来的部署合同;Kubernetes 双网关配置不会自动继承其支持声明。 + +```mermaid +flowchart LR + C[MCP 客户端] --> S[Kubernetes Service] + S --> G0[网关 0] + S --> G1[网关 1] + G0 --> M[(PostgreSQL metadata)] + G1 --> M + G0 --> A[(PostgreSQL audit)] + G1 --> A + G0 --> P0[(本节点 B2 标识)] + G1 --> P1[(本节点 B2 标识)] +``` + +## 安全与运行合同 + +| 关注点 | 设计决定 | 失败行为 | +| --- | --- | --- | +| 身份与租户 | 共享同一 `AGENTSQL_SECRET`、PostgreSQL metadata、Agent/人员授权和单独审计库;运行密钥由 Kubernetes Secret 注入,不写入 ConfigMap。 | Secret 缺失或不一致时不启动;跨 Agent/租户负向用例必须复验。 | +| B2 实例身份 | 每个 StatefulSet ordinal 使用独立 PVC,持久保存 `instance_id_file`。绝不共享同一实例文件或本地 WAL。 | PVC 不可写、重复实例标识或租约异常时必须拒绝 B2 能力,不能降级为无列授权查询。 | +| 无状态路由 | `mcp.http.stateful:false`、`event_store_enabled:false`、`mcp.sessions.enabled:false`、`mcp.transactions.postgres:false`。 | 客户端若要求会话、事件恢复或 B5,按协议返回不支持或拒绝;不伪装为跨节点续接。 | +| 请求预算 | 当前普通 MCP Agent QPS 限流按进程计算。固定两副本各配置不超过声明集群 QPS 的一半,聚合上界不超过声明值;分布不均可能提前限流。 | 不自动扩容;改变副本数前需重新计算预算并验收。 | +| 审计与依赖 | 运行副本必须连通 metadata 和 audit;`/readyz` 作为 Service 准入,`/healthz` 作为进程存活。 | audit/metadata 故障时拒绝受控请求;审计恢复后才恢复交付。不得把本地缓存视为持久审计。 | +| 迁移与版本 | 迁移 Job 单独使用 DDL 凭据,运行副本仅执行只读 `agentsqlctl migrate --check`;两个 PostgreSQL 库成套备份。B2 运行时校验二进制摘要,跨版本不能同时活跃:固定 `OnDelete`,升级须在维护窗口停止两个副本并等旧租约过期后再启动新版。 | schema 缺失、落后或超前时 Pod 不可用;混合二进制摘要时新副本拒绝就绪。停止切换并按快照恢复。 | + +## 交付内容 + +1. 版本固定的 Kubernetes 清单:ConfigMap、双副本 StatefulSet、ClusterIP Service、PDB、节点分散约束、持久卷、资源请求和限制、非 root 安全上下文、健康探针、Secret 引用。 +2. 迁移及部署前检查脚本与操作文档。检查 PostgreSQL 双库、schema、Secret 名称、固定副本和部署模式,不显示凭据内容。 +3. 管理与运维说明:每节点版本、实例 ID、就绪/审计状态定位;明确 Service 聚合视角与单节点诊断视角。 +4. 双节点真实验收:允许/拒绝/脱敏/持久审计、两节点轮询、单 Pod 故障、metadata/audit 故障关闭与恢复、维护窗口升级和 v1.9.0 正式包回退、成套备份恢复。 + +## 发布门槛及边界 + +必须取得同一提交的 Go/Web/漏洞检查 CI、真实 Kubernetes 双节点和 PostgreSQL 验收、v1.9.0 正式版升级恢复、双架构签名资产与匿名镜像拉取、GitHub Release、官网和 Live Demo 公网复核。Live Demo 采用何种拓扑须照实说明,不能以单节点 Demo 代替双节点能力证据。 + +不包含跨区域一致性、PostgreSQL 自动故障转移、B5 连接跨节点续接、无限水平扩容或生产 SLA。真实客户场景和负载数据尚未取得;公开支持范围仅覆盖已测试 Kubernetes、PostgreSQL、MCP 客户端版本及拓扑。 diff --git a/internal/adminapi/handler.go b/internal/adminapi/handler.go index 90c1abf8..6bce5580 100644 --- a/internal/adminapi/handler.go +++ b/internal/adminapi/handler.go @@ -253,6 +253,7 @@ func NewHandler(deps Deps, logger zerolog.Logger) (http.Handler, error) { mux.HandleFunc("PUT /api/v1/policies/{id}", handler.policiesUpdate) mux.HandleFunc("DELETE /api/v1/policies/{id}", handler.policiesDelete) mux.HandleFunc("POST /api/v1/policies/{id}/bind-mysql", handler.policiesBindMySQL) + mux.HandleFunc("POST /api/v1/policies/{id}/bind-postgres", handler.policiesBindPostgres) mux.HandleFunc("GET /api/v1/rules", handler.rulesList) mux.HandleFunc("POST /api/v1/rules", handler.rulesCreate) mux.HandleFunc("PUT /api/v1/rules/{id}", handler.rulesUpdate) diff --git a/internal/adminapi/postgres_column_binding.go b/internal/adminapi/postgres_column_binding.go new file mode 100644 index 00000000..1c877d7d --- /dev/null +++ b/internal/adminapi/postgres_column_binding.go @@ -0,0 +1,41 @@ +package adminapi + +import ( + "errors" + "net/http" + + "github.com/cuipengdba/agentsql/internal/store" +) + +// policiesBindPostgres accepts an enrollment SELECT, never executes it, and +// returns only the committed policy projection. It currently supports the +// PostgreSQL closed binder for one regular table. +func (handler *Handler) policiesBindPostgres(writer http.ResponseWriter, request *http.Request) { + id, expected, status := parsePolicyIfMatch(request) + if status != 0 { + handler.fail(writer, status, http.StatusText(status)) + return + } + if id != request.PathValue("id") { + handler.fail(writer, http.StatusBadRequest, "If-Match policy does not match request path") + return + } + var input struct { + SQL string `json:"sql"` + } + if err := decodeJSON(writer, request, &input); err != nil || input.SQL == "" { + handler.fail(writer, http.StatusBadRequest, "invalid enrollment input") + return + } + bound, err := handler.deps.Runtime.BindPostgresColumnPolicy(request.Context(), id, expected, input.SQL) + if err != nil { + if errors.Is(err, store.ErrRevisionMismatch) { + handler.fail(writer, http.StatusPreconditionFailed, "policy revision changed or cannot be enrolled") + return + } + handler.fail(writer, http.StatusConflict, "PostgreSQL column catalog or permission enrollment failed") + return + } + writer.Header().Set("ETag", policyETag(bound.ID, bound.Revision)) + handler.ok(writer, policyToView(bound)) +} diff --git a/internal/authorizedexecute/postgres_policy_binding.go b/internal/authorizedexecute/postgres_policy_binding.go new file mode 100644 index 00000000..899905d1 --- /dev/null +++ b/internal/authorizedexecute/postgres_policy_binding.go @@ -0,0 +1,62 @@ +package authorizedexecute + +import ( + "context" + + "github.com/cuipengdba/agentsql/internal/authorizedexecute/internal/businessdb" + "github.com/cuipengdba/agentsql/internal/model" +) + +// PostgresPolicyBindingLease holds the business catalog locks until the +// control-plane policy transaction has committed or rolled back. It cannot +// execute the SELECT used to prove the column identities. +type PostgresPolicyBindingLease struct { + Enrollment PostgresPolicyEnrollment + prepared *businessdb.PostgresClosedPrepared +} + +func (lease *PostgresPolicyBindingLease) Close() error { + if lease == nil || lease.prepared == nil { + return nil + } + err := lease.prepared.Close(context.Background()) + lease.prepared = nil + return err +} + +// BindPostgresPolicySelect revalidates one closed-binder SELECT while retaining +// its relation locks. Native-extension enrollment requires a separate locked +// implementation and is deliberately refused here. +func (gateway *Gateway) BindPostgresPolicySelect(ctx context.Context, datasource model.Datasource, secret []byte, rawSQL string, limits Limits) (*PostgresPolicyBindingLease, error) { + if datasource.DBType != "postgres" { + return nil, &AuthError{Reason: ReasonDatasourceUnsupported} + } + if err := ValidatePreParse(rawSQL, nil, limits); err != nil { + return nil, err + } + capability, err := gateway.ProbePostgresB2Modes(ctx, datasource, secret) + if err != nil { + return nil, err + } + if capability.NativeAvailable || capability.Mode != "CATALOG_CLOSED_V1" { + return nil, &AuthError{Reason: ReasonBinderModeUnsupported} + } + opened, err := gateway.open(datasource, secret) + if err != nil { + return nil, fixedExecutionError(err) + } + postgres, ok := opened.(*businessdb.PostgresExecutor) + if !ok { + return nil, &AuthError{Reason: ReasonDatasourceUnsupported} + } + prepared, err := postgres.BindClosedSelect(ctx, businessdb.BindRequest{ + RawSQL: rawSQL, Identity: businessdb.SemanticIdentity{DatasourceIdentity: datasource.ID}, + }, NewBudget(limits)) + if err != nil { + return nil, StableError(err) + } + return &PostgresPolicyBindingLease{ + Enrollment: publicPolicyEnrollment(capability.Mode, prepared.Program().Facts), + prepared: prepared, + }, nil +} diff --git a/internal/bootstrap/b2_runtime.go b/internal/bootstrap/b2_runtime.go index d9c356f0..2d2fdce0 100644 --- a/internal/bootstrap/b2_runtime.go +++ b/internal/bootstrap/b2_runtime.go @@ -77,7 +77,22 @@ func featureOffB2Status() B2Status { } func activateB2Runtime(ctx context.Context, metadata *store.Store, gateway *executor.Gateway, secret []byte, instanceID string, lease, interval time.Duration, mysqlOptions ...mysqlB2RuntimeOptions) (*b2Runtime, error) { - return evaluateB2Runtime(ctx, metadata, gateway, secret, instanceID, lease, interval, true, mysqlOptions...) + // Two fresh replicas may observe the same activation ETag. The first + // activation advances the fence revision, so the other must re-read the + // readiness snapshot before retrying. Persistent digest incompatibility + // or dependency failure remains closed after this bounded window. + for attempt := 0; ; attempt++ { + manager, err := evaluateB2Runtime(ctx, metadata, gateway, secret, instanceID, lease, interval, true, mysqlOptions...) + if err == nil || manager == nil || manager.status.Reason != B2ReasonControlFenceUnavailable || attempt >= 9 { + return manager, err + } + wait := time.Duration(attempt+1) * 50 * time.Millisecond + select { + case <-ctx.Done(): + return manager, errors.Join(err, ctx.Err()) + case <-time.After(wait): + } + } } // dryRunB2Runtime executes the same probes and phase-one snapshot as diff --git a/internal/bootstrap/postgres_column_enrollment.go b/internal/bootstrap/postgres_column_enrollment.go new file mode 100644 index 00000000..f31bad80 --- /dev/null +++ b/internal/bootstrap/postgres_column_enrollment.go @@ -0,0 +1,133 @@ +package bootstrap + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + executor "github.com/cuipengdba/agentsql/internal/authorizedexecute" + "github.com/cuipengdba/agentsql/internal/model" + "github.com/cuipengdba/agentsql/internal/store" + "github.com/google/uuid" +) + +// BindPostgresColumnPolicy enrolls a regular PostgreSQL table using the +// closed binder. Discovery holds no control lock; the final catalog proof +// retains the business relation lock through the metadata CAS and commit. +func (runtime *Runtime) BindPostgresColumnPolicy(ctx context.Context, policyID string, expectedRevision int64, rawSQL string) (model.Policy, error) { + if runtime == nil || runtime.Store == nil || runtime.business == nil || policyID == "" || expectedRevision <= 0 || strings.TrimSpace(rawSQL) == "" { + return model.Policy{}, errors.New("PostgreSQL column enrollment is unavailable or invalid") + } + policy, err := runtime.Store.Policies().Get(ctx, policyID) + if err != nil { + return model.Policy{}, err + } + if policy.Revision != expectedRevision || policy.Action != "allow" || policy.ObjectType != "column" || + policy.RowFilter != nil || policy.Columns == nil { + return model.Policy{}, store.ErrRevisionMismatch + } + requested := make(map[string]struct{}) + for _, raw := range strings.Split(*policy.Columns, ",") { + name := strings.TrimSpace(raw) + if name == "" || name == "*" || len(requested) >= 128 { + return model.Policy{}, errors.New("invalid PostgreSQL column policy") + } + if _, duplicate := requested[name]; duplicate { + return model.Policy{}, errors.New("duplicate PostgreSQL column grant") + } + requested[name] = struct{}{} + } + datasource, err := runtime.Store.Datasources().Get(ctx, policy.DatasourceID) + if err != nil || datasource.DBType != "postgres" { + return model.Policy{}, fmt.Errorf("policy datasource is not PostgreSQL: %w", err) + } + var discovered executor.PostgresPolicyEnrollment + discover := func(phase context.Context) error { + candidate, err := runtime.business.EnrollPostgresPolicySelect(phase, datasource, runtime.secret, rawSQL, executor.DefaultLimits) + if err != nil { + return err + } + if _, err := postgresColumnEnrollment(policy, requested, candidate, ""); err != nil { + return err + } + discovered = candidate + return nil + } + revalidate := func(phase context.Context) (store.RevalidatedColumnBinding, error) { + lease, err := runtime.business.BindPostgresPolicySelect(phase, datasource, runtime.secret, rawSQL, executor.DefaultLimits) + if err != nil { + return store.RevalidatedColumnBinding{}, err + } + if !reflect.DeepEqual(discovered, lease.Enrollment) { + _ = lease.Close() + return store.RevalidatedColumnBinding{}, errors.New("PostgreSQL catalog changed between discovery and revalidation") + } + bindingID := uuid.NewString() + bound, err := postgresColumnEnrollment(policy, requested, lease.Enrollment, bindingID) + if err != nil { + _ = lease.Close() + return store.RevalidatedColumnBinding{}, err + } + bound.Release = lease.Close + return bound, nil + } + return runtime.Store.Policies().FinalizeColumnBindingTwoPhase(ctx, policyID, expectedRevision, discover, revalidate) +} + +func postgresColumnEnrollment(policy model.Policy, requested map[string]struct{}, enrollment executor.PostgresPolicyEnrollment, bindingID string) (store.RevalidatedColumnBinding, error) { + if enrollment.Mode != "CATALOG_CLOSED_V1" || enrollment.StatementClass != "SELECT" || + len(enrollment.Relations) != 1 || len(enrollment.ColumnUses) == 0 || len(enrollment.WriteTargets) != 0 { + return store.RevalidatedColumnBinding{}, errors.New("PostgreSQL closed SELECT enrollment is required") + } + relation := enrollment.Relations[0] + if relation.DatabaseOID == 0 || relation.RelationOID == 0 || relation.Kind != 'r' || + relation.Schema+"."+relation.Name != policy.ObjectName || relation.CatalogFingerprint == "" { + return store.RevalidatedColumnBinding{}, errors.New("PostgreSQL relation identity mismatch") + } + columns := make(map[string]executor.PostgresPolicyColumnUse, len(requested)) + output := make(map[string]bool, len(requested)) + for _, use := range enrollment.ColumnUses { + if _, allowed := requested[use.Name]; !allowed || use.RelationOID != relation.RelationOID || + use.Attnum <= 0 || (use.Usage != "output" && use.Usage != "reference") { + return store.RevalidatedColumnBinding{}, errors.New("PostgreSQL enrollment includes an unrequested column") + } + if prior, duplicate := columns[use.Name]; duplicate { + if prior.Attnum != use.Attnum || prior.TypeOID != use.TypeOID || prior.TypeModifier != use.TypeModifier || prior.CollationOID != use.CollationOID { + return store.RevalidatedColumnBinding{}, errors.New("PostgreSQL column identity changed") + } + } else { + columns[use.Name] = use + } + if use.Usage == "output" { + output[use.Name] = true + } + } + if len(columns) != len(requested) { + return store.RevalidatedColumnBinding{}, errors.New("PostgreSQL enrollment is missing a requested column") + } + for name := range requested { + if !output[name] { + return store.RevalidatedColumnBinding{}, errors.New("PostgreSQL requested column is not proven in SELECT output") + } + } + stable := executor.PostgresStableObjectID(relation.DatabaseOID, relation.RelationOID) + fingerprint := relation.CatalogFingerprint + result := store.RevalidatedColumnBinding{Binding: model.RelationPolicyBinding{ + ID: bindingID, PolicyID: policy.ID, DatasourceID: policy.DatasourceID, + SchemaName: relation.Schema, RelationName: relation.Name, + StableObjectID: &stable, CatalogFingerprint: &fingerprint, Status: "healthy", Revision: 1, + }} + for _, raw := range strings.Split(*policy.Columns, ",") { + use := columns[strings.TrimSpace(raw)] + for _, usage := range []string{"output", "reference"} { + result.Permissions = append(result.Permissions, model.PolicyColumnPermission{ + PolicyID: policy.ID, RelationEnrollmentID: bindingID, ColumnOrdinal: int(use.Attnum), + ColumnName: use.Name, ColumnTypeDigest: executor.PostgresColumnTypeDigest(use.TypeOID, use.TypeModifier, use.CollationOID), + Usage: usage, + }) + } + } + return result, nil +} diff --git a/internal/bootstrap/postgres_column_enrollment_test.go b/internal/bootstrap/postgres_column_enrollment_test.go new file mode 100644 index 00000000..145d3ce4 --- /dev/null +++ b/internal/bootstrap/postgres_column_enrollment_test.go @@ -0,0 +1,46 @@ +package bootstrap + +import ( + "testing" + + executor "github.com/cuipengdba/agentsql/internal/authorizedexecute" + "github.com/cuipengdba/agentsql/internal/model" + "github.com/stretchr/testify/require" +) + +func TestPostgresColumnEnrollmentRejectsUnprovenIdentity(t *testing.T) { + columns := "id,phone" + policy := model.Policy{ID: "p", DatasourceID: "d", ObjectName: "public.customers", Columns: &columns} + relation := executor.PostgresPolicyRelation{DatabaseOID: 1, RelationOID: 2, Schema: "public", Name: "customers", Kind: 'r', CatalogFingerprint: "catalog-v1"} + base := executor.PostgresPolicyEnrollment{Mode: "CATALOG_CLOSED_V1", StatementClass: "SELECT", + Relations: []executor.PostgresPolicyRelation{relation}, ColumnUses: []executor.PostgresPolicyColumnUse{ + {RelationOID: 2, Attnum: 1, Name: "id", TypeOID: 23, TypeModifier: -1, Usage: "output"}, + {RelationOID: 2, Attnum: 2, Name: "phone", TypeOID: 25, TypeModifier: -1, CollationOID: 100, Usage: "output"}, + }} + requested := map[string]struct{}{"id": {}, "phone": {}} + bound, err := postgresColumnEnrollment(policy, requested, base, "binding-1") + require.NoError(t, err) + require.Equal(t, "pg:1:2", *bound.Binding.StableObjectID) + require.Len(t, bound.Permissions, 4) + require.Equal(t, "pg:type=25;typmod=-1;collation=100", bound.Permissions[2].ColumnTypeDigest) + + for name, mutate := range map[string]func(*executor.PostgresPolicyEnrollment){ + "unknown relation": func(v *executor.PostgresPolicyEnrollment) { v.Relations[0].Name = "internal_notes" }, + "recreated object": func(v *executor.PostgresPolicyEnrollment) { v.ColumnUses[0].RelationOID = 3 }, + "extra reference": func(v *executor.PostgresPolicyEnrollment) { + v.ColumnUses = append(v.ColumnUses, executor.PostgresPolicyColumnUse{RelationOID: 2, Attnum: 3, Name: "secret", TypeOID: 25, Usage: "reference"}) + }, + "missing column": func(v *executor.PostgresPolicyEnrollment) { v.ColumnUses = v.ColumnUses[:1] }, + "reference only": func(v *executor.PostgresPolicyEnrollment) { v.ColumnUses[1].Usage = "reference" }, + "native mode": func(v *executor.PostgresPolicyEnrollment) { v.Mode = "NATIVE_V1" }, + } { + t.Run(name, func(t *testing.T) { + candidate := base + candidate.Relations = append([]executor.PostgresPolicyRelation(nil), base.Relations...) + candidate.ColumnUses = append([]executor.PostgresPolicyColumnUse(nil), base.ColumnUses...) + mutate(&candidate) + _, err := postgresColumnEnrollment(policy, requested, candidate, "binding-1") + require.Error(t, err) + }) + } +} diff --git a/internal/rbac/bootstrap_postgres_e2e_test.go b/internal/rbac/bootstrap_postgres_e2e_test.go new file mode 100644 index 00000000..d6f94668 --- /dev/null +++ b/internal/rbac/bootstrap_postgres_e2e_test.go @@ -0,0 +1,72 @@ +package rbac + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/cuipengdba/agentsql/internal/store" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + postgrescontainer "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +func TestConcurrentPostgresBootstrapE2E(t *testing.T) { + if testing.Short() { + t.Skip("requires a real PostgreSQL container") + } + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + container, err := postgrescontainer.Run(ctx, "postgres:18", + postgrescontainer.WithDatabase("agentsql_bootstrap"), + postgrescontainer.WithUsername("agentsql_bootstrap"), + postgrescontainer.WithPassword("bootstrap-e2e-password"), + postgrescontainer.BasicWaitStrategies(), + ) + require.NoError(t, err) + testcontainers.CleanupContainer(t, container) + host, err := container.Host(ctx) + require.NoError(t, err) + port, err := container.MappedPort(ctx, "5432/tcp") + require.NoError(t, err) + dsn := fmt.Sprintf("postgres://agentsql_bootstrap:bootstrap-e2e-password@%s:%s/agentsql_bootstrap?sslmode=disable", host, port.Port()) + options := store.MetadataOptions{Driver: store.DialectPostgres, PostgresDSN: dsn, MaxOpenConns: 4, MaxIdleConns: 2, AutoMigrate: true} + const secret = "0123456789abcdef0123456789abcdef" + seed, err := store.OpenMetadata(ctx, options, []byte(secret)) + require.NoError(t, err) + require.NoError(t, seed.Close()) + + const replicas = 8 + services := make([]*Service, 0, replicas) + for range replicas { + options.AutoMigrate = false + opened, openErr := store.OpenMetadata(ctx, options, []byte(secret)) + require.NoError(t, openErr) + t.Cleanup(func() { require.NoError(t, opened.Close()) }) + services = append(services, NewService(opened.RBAC())) + } + + start := make(chan struct{}) + results := make(chan error, replicas) + var workers sync.WaitGroup + for _, service := range services { + workers.Add(1) + go func(service *Service) { + defer workers.Done() + <-start + results <- service.Bootstrap(ctx, "admin", "concurrent-bootstrap-password") + }(service) + } + close(start) + workers.Wait() + close(results) + for bootstrapErr := range results { + require.NoError(t, bootstrapErr) + } + principal, err := services[0].Principal(ctx, DefaultTenantID, BootstrapUserID) + require.NoError(t, err) + require.True(t, principal.Has(PermissionRoleManage)) + require.True(t, principal.Has(PermissionAuditView)) +} diff --git a/internal/rbac/service.go b/internal/rbac/service.go index 633d7825..1ff50e07 100644 --- a/internal/rbac/service.go +++ b/internal/rbac/service.go @@ -9,6 +9,7 @@ import ( "fmt" "sort" "strings" + "time" "github.com/cuipengdba/agentsql/internal/model" "github.com/cuipengdba/agentsql/internal/store" @@ -101,6 +102,39 @@ func VerifyPassword(hash, password string) bool { // exactly when missing. The password is accepted from the existing environment // credential source and is persisted only as a bcrypt hash. func (service *Service) Bootstrap(ctx context.Context, username, password string) error { + var err error + for attempt := 0; attempt < 10; attempt++ { + err = service.bootstrapOnce(ctx, username, password) + if err == nil || !bootstrapRace(err) { + return err + } + wait := time.Duration(attempt+1) * 50 * time.Millisecond + select { + case <-ctx.Done(): + return errors.Join(err, ctx.Err()) + case <-time.After(wait): + } + } + return err +} + +// Concurrent gateways can both observe missing bootstrap rows. A bounded +// retry re-reads the winner's committed rows instead of treating that normal +// first-start race as a permanently failed gateway. +func bootstrapRace(err error) bool { + if err == nil { + return false + } + message := strings.ToLower(err.Error()) + return strings.Contains(message, "sqlstate 23505") || + strings.Contains(message, "sqlstate 40001") || + strings.Contains(message, "sqlstate 40p01") || + strings.Contains(message, "unique constraint failed") || + strings.Contains(message, "database is locked") || + strings.Contains(message, "sqlite_busy") +} + +func (service *Service) bootstrapOnce(ctx context.Context, username, password string) error { if service == nil || service.repository == nil || ctx == nil { return errors.New("bootstrap RBAC: service is unavailable") } diff --git a/internal/version/version.go b/internal/version/version.go index 7f8b1bec..43be40df 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,4 +2,4 @@ package version // Version is overridden in release builds through -ldflags -X. -var Version = "v1.9.0" +var Version = "v2.0.0" diff --git a/scripts/build-ghcr-multiarch.ps1 b/scripts/build-ghcr-multiarch.ps1 index 865327a5..dc91b113 100644 --- a/scripts/build-ghcr-multiarch.ps1 +++ b/scripts/build-ghcr-multiarch.ps1 @@ -10,7 +10,7 @@ [CmdletBinding()] param( [ValidatePattern('^v[0-9]+\.[0-9]+\.[0-9]+$')] - [string]$Version = 'v1.9.0', + [string]$Version = 'v2.0.0', [string]$Builder = 'agentsql-multiarch', diff --git a/scripts/build-release-linux.sh b/scripts/build-release-linux.sh index d64c0d6e..c5717073 100644 --- a/scripts/build-release-linux.sh +++ b/scripts/build-release-linux.sh @@ -3,7 +3,7 @@ set -eu ARCH=${ARCH:-amd64} -VERSION=${VERSION:-v1.9.0} +VERSION=${VERSION:-v2.0.0} GO_VERSION=${GO_VERSION:-1.26.9} GOPROXY=${GOPROXY:-https://goproxy.cn,direct} YASHAN_CLIENT_VERSION=${YASHAN_CLIENT_VERSION:-23.4.7.100} diff --git a/scripts/package-release.sh b/scripts/package-release.sh index bd0ef9bf..b71e06ca 100644 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -3,7 +3,7 @@ set -eu umask 022 -VERSION=${VERSION:-v1.9.0} +VERSION=${VERSION:-v2.0.0} ARCH=${ARCH:-amd64} SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-1704067200} diff --git a/scripts/release-dryrun.ps1 b/scripts/release-dryrun.ps1 index f7313818..061eeda8 100644 --- a/scripts/release-dryrun.ps1 +++ b/scripts/release-dryrun.ps1 @@ -4,7 +4,7 @@ [CmdletBinding()] param( [ValidatePattern('^v[0-9]+\.[0-9]+\.[0-9]+$')] - [string]$Version = 'v1.9.0', + [string]$Version = 'v2.0.0', [string]$OutputDirectory, diff --git a/website/README.md b/website/README.md index ae439b36..c0a890ca 100644 --- a/website/README.md +++ b/website/README.md @@ -29,7 +29,7 @@ bash ./deploy/verify-static.sh http://127.0.0.1:8080 stage-a - 只发布 `public/` 内容;不得把 `deploy/`、仓库根目录、`.git/` 或环境文件放入文档根。 - 阶段 A 只监听 `127.0.0.1:8080`,通过 SSH 隧道预览;备案、DNS 和防火墙就绪后才切换阶段 B。 -- v1.0.0 页面仅在对应 GitHub Release、双架构镜像和版本化 PDF 通过验收后部署;Release 按钮固定指向 `https://github.com/cuipengdba/agentsql/releases/tag/v1.0.0`。 +- 当前版本页面仅在对应 GitHub Release、双架构镜像和版本化 PDF 通过验收后部署;Release 按钮必须指向同版本正式发布页。公开 Demo 的部署拓扑须在页面明确说明。 - 站点没有 Cookie 或第三方统计。Caddy 访问日志默认按配置保留 720 小时。 - 开源授权链接指向仓库 `LICENSE`;商业授权链接指向已存在的 `COMMERCIAL-LICENSE.md`。 diff --git a/website/deploy/verify-static.sh b/website/deploy/verify-static.sh index 81c289d4..4b156f0a 100644 --- a/website/deploy/verify-static.sh +++ b/website/deploy/verify-static.sh @@ -197,9 +197,9 @@ fi pdf_fail=0 for pdf in \ - assets/docs/agentsql-getting-started-v1.9.0.pdf \ - assets/docs/agentsql-user-guide-v1.9.0.pdf \ - assets/docs/agentsql-mcp-integrations-v1.9.0.pdf; do + assets/docs/agentsql-getting-started-v2.0.0.pdf \ + assets/docs/agentsql-user-guide-v2.0.0.pdf \ + assets/docs/agentsql-mcp-integrations-v2.0.0.pdf; do pdf_path="$SITE_ROOT/$pdf" if [[ ! -f $pdf_path ]]; then printf ' 缺失 PDF: %s\n' "$pdf" diff --git a/website/public/assets/docs/agentsql-getting-started-v2.0.0.pdf b/website/public/assets/docs/agentsql-getting-started-v2.0.0.pdf new file mode 100644 index 00000000..91249cd1 Binary files /dev/null and b/website/public/assets/docs/agentsql-getting-started-v2.0.0.pdf differ diff --git a/website/public/assets/docs/agentsql-mcp-integrations-v2.0.0.pdf b/website/public/assets/docs/agentsql-mcp-integrations-v2.0.0.pdf new file mode 100644 index 00000000..14d8f78d Binary files /dev/null and b/website/public/assets/docs/agentsql-mcp-integrations-v2.0.0.pdf differ diff --git a/website/public/assets/docs/agentsql-user-guide-v2.0.0.pdf b/website/public/assets/docs/agentsql-user-guide-v2.0.0.pdf new file mode 100644 index 00000000..02a42d35 Binary files /dev/null and b/website/public/assets/docs/agentsql-user-guide-v2.0.0.pdf differ diff --git a/website/public/demo/index.html b/website/public/demo/index.html index e248846e..ebabac30 100644 --- a/website/public/demo/index.html +++ b/website/public/demo/index.html @@ -40,7 +40,7 @@
-
AgentSQL v1.9.0 · Live Demo 安全入口
+
AgentSQL v2.0.0 · Live Demo 安全入口

先确认边界,再进入演示

此环境只用于体验 AgentSQL 的控制台与六个预置安全剧本,不是生产服务,也不连接任何真实业务数据库。

@@ -56,7 +56,7 @@

先确认边界,再进入演示

我已了解,进入演示 返回官网
-

控制台由独立演示源提供;入口地址保持为 https://agentsql.cn/demo/。公开演示未启用可选的人员委托模式;版本边界见发布说明。

+

控制台由独立的单网关演示源提供;入口地址保持为 https://agentsql.cn/demo/。公开演示未启用双网关 Kubernetes 配置或可选人员委托模式;版本边界见发布说明。

diff --git a/website/public/index.html b/website/public/index.html index e570448b..32f51eb9 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -75,14 +75,14 @@
-
v1.9.0
+
v2.0.0

AI Agent 的数据库安全网关

AgentSQL(中文名:智盾)是面向 AI Agent 的数据库安全网关与 MCP Server。模型发出的 PostgreSQL / MySQL 请求在到达数据库前,经过身份认证、SQL 解析、静态与动态风险检查、四态决策与受控执行;结果按配置脱敏,并在应用层留下持久审计。

-

v1.9.0 在控制台接入向导集中提供 Dify 与 MCP Inspector 的配置说明、目标 SQL 的离线策略解释和真实调用审计定位。数据库支持范围保持不变;Live Demo 使用同版合成数据环境。

+

v2.0.0 为固定双网关部署提供 Kubernetes 清单、维护窗口升级与故障恢复指南,并为 PostgreSQL 单表列策略增加正式登记 API。数据库支持范围保持不变;公开 Live Demo 使用同版单网关合成数据环境,双网关能力以隔离 Kubernetes 验收为准。

v0.5.0 起 Apache-2.0 开源 · Go 单二进制交付(glibc / cgo 构建)· 自托管 · 无第三方遥测依赖

它不是 BI,也不是 ORM 或 Text2SQL,而是模型与数据库之间一道默认拒绝的安全闸门。

@@ -200,24 +200,24 @@

让模型的数据库访问可约束、可解释、可追溯

文档 / 下载

从本地演示到 MCP 接入

按真实部署状态选择入口。文档对应源码仓库 github.com/cuipengdba/agentsql。

-
01

快速上手

用本地 Live Demo 看效果,再按默认拒绝的顺序接入自己的数据库。

-
02

使用手册

按控制台菜单了解审计、Agent、数据源、权限、规则、审批与脱敏边界。

-
03

MCP 接入指南

对照客户端能力选择现有 /mcp、独立无状态 /mcp/2026-07-28 或 stdio,查看工具与拒绝边界。

+
01

快速上手

用本地 Live Demo 看效果,再按默认拒绝的顺序接入自己的数据库。

+
02

使用手册

按控制台菜单了解审计、Agent、数据源、权限、规则、审批与脱敏边界。

+
03

MCP 接入指南

对照客户端能力选择现有 /mcp、独立无状态 /mcp/2026-07-28 或 stdio,查看工具与拒绝边界。

-

更多资料

部署指南 · 在线演示说明

v1.9.0 提供 Linux amd64 / arm64 安装包与多架构镜像;Live Demo 入口仅使用合成数据。

GHCR 多架构拉取(自动匹配 amd64 / arm64)

docker pull ghcr.io/cuipengdba/agentsql:v1.9.0

一键安装(Linux amd64 / arm64)

curl -fsSL https://github.com/cuipengdba/agentsql/releases/download/v1.9.0/install.sh | sudo sh -s -- install --version v1.9.0

v0.4.0 起提供 linux/arm64 原生 glibc 包 · x86_64 / amd64 或 aarch64 / arm64 · glibc ≥ 2.28 · systemd · 需要 sudo/root

- +

更多资料

部署指南 · 在线演示说明

v2.0.0 提供 Linux amd64 / arm64 安装包与多架构镜像;Live Demo 入口仅使用合成数据。

GHCR 多架构拉取(自动匹配 amd64 / arm64)

docker pull ghcr.io/cuipengdba/agentsql:v2.0.0

一键安装(Linux amd64 / arm64)

curl -fsSL https://github.com/cuipengdba/agentsql/releases/download/v2.0.0/install.sh | sudo sh -s -- install --version v2.0.0

v0.4.0 起提供 linux/arm64 原生 glibc 包 · x86_64 / amd64 或 aarch64 / arm64 · glibc ≥ 2.28 · systemd · 需要 sudo/root

+
-

兼容性

v1.9.0 兼容矩阵

本版改善客户端接入,数据库支持范围沿用 v1.8.0;下表按实际能力与指定环境证据分档。

+

兼容性

v2.0.0 兼容矩阵

本版增加固定双网关部署和 PostgreSQL 列策略登记;数据库适配范围沿用 v1.9.0。下表按实际能力与指定环境证据分档。

8 大类 / 29 款已注册瀚高 HighGo 已计入;另列金仓 KingbaseES V9 的指定实例 PG 协议验证
- + @@ -266,11 +266,11 @@

让模型的数据库访问可约束、可解释、可追溯

生态与兼容

国产数据库生态适配进展

v1.8.0 在 Vastbase G100 Build 9 PG 模式的指定实例补充 TLS 服务端身份校验、证书与凭据轮换。其他版本、模式、正式主备切换和完整生产支持仍须单独验证。欢迎数据库厂商联合验证。

-

国产数据库生态适配矩阵(v1.9.0)

+

国产数据库生态适配矩阵(v2.0.0)

表格可左右滑动,查看完整验证说明。

AgentSQL v1.9.0 数据库兼容矩阵:8 大类、29 款已注册数据库,金仓 KingbaseES V9 另列,共 30 行。AgentSQL v2.0.0 数据库兼容矩阵:8 大类、29 款已注册数据库,金仓 KingbaseES V9 另列,共 30 行。
类别数据库适配状态边界说明
关系型PostgreSQL 14–18🟢 已适配 · 完整防护全链路实测通过
- + @@ -300,11 +300,11 @@

让模型的数据库访问可约束、可解释、可追溯

开始使用

快速开始

在 Docker 与依赖网络已就绪时通常数分钟可完成(受网络环境影响)。GHCR tag 会自动匹配 linux/amd64 或 linux/arm64;源码路径仍以仓库 README 为准。

GHCRamd64 / arm64
# Docker 自动选择当前平台镜像
-docker pull ghcr.io/cuipengdba/agentsql:v1.9.0
+docker pull ghcr.io/cuipengdba/agentsql:v2.0.0
 
 # 推荐使用仓库 quickstart 生成凭据并启动
-curl -fsSL https://raw.githubusercontent.com/cuipengdba/agentsql/v1.9.0/scripts/quickstart.sh -o quickstart.sh
-sh quickstart.sh --version v1.9.0
默认仅绑定 127.0.0.1:7780
+curl -fsSL https://raw.githubusercontent.com/cuipengdba/agentsql/v2.0.0/scripts/quickstart.sh -o quickstart.sh +sh quickstart.sh --version v2.0.0
默认仅绑定 127.0.0.1:7780
Live Demo合成数据
# 复制演示环境样例并替换占位凭据
 cp examples/docker/demo.env.example demo/demo.env
 
@@ -354,7 +354,8 @@ 

让模型的数据库访问可约束、可解释、可追溯

  • v1.6.0查询超时、取消与审计故障关闭已发布阶段
  • v1.7.0瀚高限定只读与金仓指定实例证书校验已发布阶段
  • v1.8.0Vastbase 指定实例可校验 TLS 与轮换已发布阶段
  • -
  • v1.9.0Dify / Inspector 接入向导与审计定位当前正式版本
  • +
  • v1.9.0Dify / Inspector 接入向导与审计定位已发布阶段
  • +
  • v2.0.0固定双网关部署与 PostgreSQL 列策略登记当前正式版本
  • 下一阶段国产与商业数据库适配
  • 后续方向企业身份与审计增强SSO/RBAC、WORM 选项
  • 后续方向高可用与集中管控
  • diff --git a/website/tools/build-docs-pdf.mjs b/website/tools/build-docs-pdf.mjs index c53c2612..e2bed769 100644 --- a/website/tools/build-docs-pdf.mjs +++ b/website/tools/build-docs-pdf.mjs @@ -16,7 +16,7 @@ import { tmpdir } from 'node:os'; import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -const VERSION = 'v1.9.0'; +const VERSION = 'v2.0.0'; const REPOSITORY_BLOB = `https://github.com/cuipengdba/agentsql/blob/${VERSION}`; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const REPOSITORY_ROOT = resolve(SCRIPT_DIR, '..', '..'); @@ -24,24 +24,24 @@ const DOCS_ROOT = resolve(REPOSITORY_ROOT, 'docs'); const OUTPUT_DIR = resolve(REPOSITORY_ROOT, 'website', 'public', 'assets', 'docs'); const MINIMUM_PDF_BYTES = 20_000; // Fixed so repeated builds are byte-reproducible; update with the release date when it is finalized. -const FIXED_PDF_DATE = "D:20261018000000+00'00'"; +const FIXED_PDF_DATE = "D:20261019000000+00'00'"; const DOCUMENTS = [ { source: resolve(DOCS_ROOT, 'GETTING_STARTED.md'), - output: 'agentsql-getting-started-v1.9.0.pdf', + output: 'agentsql-getting-started-v2.0.0.pdf', title: '快速上手', slug: 'getting-started', }, { source: resolve(DOCS_ROOT, 'USER_GUIDE.md'), - output: 'agentsql-user-guide-v1.9.0.pdf', + output: 'agentsql-user-guide-v2.0.0.pdf', title: '使用手册', slug: 'user-guide', }, { source: resolve(DOCS_ROOT, 'INTEGRATIONS.md'), - output: 'agentsql-mcp-integrations-v1.9.0.pdf', + output: 'agentsql-mcp-integrations-v2.0.0.pdf', title: 'MCP 接入指南', slug: 'mcp-integrations', }, @@ -804,6 +804,9 @@ async function main() { 'agentsql-getting-started-v1.8.0.pdf', 'agentsql-mcp-integrations-v1.8.0.pdf', 'agentsql-user-guide-v1.8.0.pdf', + 'agentsql-getting-started-v1.9.0.pdf', + 'agentsql-mcp-integrations-v1.9.0.pdf', + 'agentsql-user-guide-v1.9.0.pdf', ]; const expectedFiles = [...DOCUMENTS.map((document) => document.output), ...archivedFiles].sort(); if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
    AgentSQL v1.9.0 国产数据库生态适配进展;状态以指定版本、镜像、拓扑及仓库证据为准。AgentSQL v2.0.0 国产数据库生态适配进展;状态以指定版本、镜像、拓扑及仓库证据为准。
    数据库适配状态说明
    中电科金仓 KingbaseES🟢 PG 协议指定子集实测通过仅 V9R1C10、PG 模式、单实例合成表:只读角色拒写,受控 SELECT、手机号脱敏、R006 拒绝和持久审计通过;v1.6.0 补齐故障关闭,v1.7.0 验证 CA 与主机名校验、错误证书拒绝、证书及账号密码轮换。其他模式、复杂查询、正式主备切换与生产负载未验证。验收记录。