From ae9b4db9029e23aab9f637e38dca30f988e451d7 Mon Sep 17 00:00:00 2001 From: JuliaEdom Date: Thu, 16 Jul 2026 00:50:14 +0300 Subject: [PATCH 1/2] docs: record live multi-tenant ClickHouse proof Audit P0-1 closed the write-key boundary and the adversarial suite is green on ClickHouse 25.3 (CI test-integration + audit stand). Drop the stale not-yet-proven claim from STATUS, security-audit, and README. --- README.md | 2 +- docs/STATUS.md | 37 ++++++++++++++----------------------- docs/security-audit.md | 2 +- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3d0261b..4875c04 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Consumers are whoever needs the number now: humans, dashboards, downstream servi - **Published release line through `v2.0.0`** on PyPI (`agentflow-runtime`, `agentflow-client`) and npm (`@yuliaedomskikh/agentflow-client`) via OIDC Trusted Publishers with SLSA provenance on every artifact - **Tested and gated** — 1,500+ unit tests plus a broad Windows no-Docker suite; CI enforces 13 required status checks (lint, schema, unit, integration, helm, perf, terraform, bandit, safety, npm-audit, trivy, contract, build-smoke) through branch protection - **Dual SDK parity** across Python and TypeScript — retries, circuit breakers, batching, pagination, contract pinning, idempotency keys, `as_of` historical reads — over sub-second entity lookups (p50 `38–55 ms`, p99 `167 ms` on local hardware) -- **Security in the hot path** — a tenant boundary that lives in each serving table's write key and is applied at a single read chokepoint ([ADR-004](docs/decisions/004-tenant-id-column-over-schema-per-tenant.md); proven against DuckDB, [not yet proven on ClickHouse](docs/STATUS.md#known-issues)), parameterized queries, `sqlglot` AST validation for NL-to-SQL, fail-closed auth, secret scrubbing, and a Bandit gate for new findings +- **Security in the hot path** — a tenant boundary that lives in each serving table's write key and is applied at a single read chokepoint ([ADR-004](docs/decisions/004-tenant-id-column-over-schema-per-tenant.md); proven against DuckDB and live ClickHouse 25.3 — see [STATUS](docs/STATUS.md#proven)), parameterized queries, `sqlglot` AST validation for NL-to-SQL, fail-closed auth, secret scrubbing, and a Bandit gate for new findings - **Production-shaped extras** — two CDC paths (hardened Debezium/Kafka Connect + a ClickHouse per-branch fan-out), on-call [runbooks](docs/runbooks/README.md), and a [narrated demo](docs/dv2-multi-branch/) of the DV2 multi-branch warehouse ## Quick start diff --git a/docs/STATUS.md b/docs/STATUS.md index acde3d8..ded6f16 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,9 +1,9 @@ # Engineering Status -> Updated: **2026-07-11** (post S13 + #183 live re-verify) · release line **`v2.0.0`** · -> `main` green across all 13 required checks. Numbers below come only from -> measured, in-repo evidence — see the linked reports for methodology and -> reproduction commands. +> Updated: **2026-07-16** (post audit P0–P2-4 merge; CH multi-tenant live) · +> release line **`v2.0.0`** · `main` green across required checks. Numbers +> below come only from measured, in-repo evidence — see the linked reports +> for methodology and reproduction commands. AgentFlow's product axis — **event → live metric** on the real streaming path (Kafka → Flink → serving bridge → ClickHouse → API with Redis push @@ -22,6 +22,7 @@ passed ones (endurance, scale, delivery topology). | 4 h endurance soak (real path + API reads) | bounded lag (peak 2 915 → 0), bridge RSS/FD flat, one faulted batch replayed exactly-once by the journal guard, **zero cache drift** | [perf/soak-s11-2026-07-10.md](perf/soak-s11-2026-07-10.md) | | At-scale on own data (S13) | **51.2 M rows / 2.87 M orders / 4 years of legend history**, analyst queries 20–730 ms, all 17 §12 invariants pass incl. full-scan GTIN validation | [perf/scale-own-data-2026-07-11.md](perf/scale-own-data-2026-07-11.md) | | Security pass (offline/unit remainder) | closed; third-party pen-test **not** claimed | [security-s12-2026-07-09.md](security-s12-2026-07-09.md), [security-audit.md](security-audit.md) | +| Multi-tenant ClickHouse write key | adversarial two-tenant suite green on live CH 25.3 (CI `test-integration` + audit stand) | [security-audit.md](security-audit.md), `tests/integration/test_clickhouse_tenant_isolation_live.py` | ## Bridge write-path throughput — burst target met @@ -44,25 +45,15 @@ stressed; the ≥ 100 eps stretch bar stays open. Semantics of the batched path ## Known issues -- **Multi-tenant ClickHouse — implemented, not yet proven live.** Tenant - isolation used to be a schema qualification that nothing provisioned: no - `CREATE SCHEMA` existed anywhere in `src/`, so on DuckDB every *authenticated* - entity read failed on a relation that was never created, and on ClickHouse the - same name meant a database nobody creates. With the qualification dropped, two - tenants sharing an `order_id` were two versions of one `ReplacingMergeTree` - row and the later insert destroyed the earlier — data loss no read filter can - undo. The boundary is now the `tenant_id` **column**, leading each serving - table's write key on both stores - ([ADR-004](decisions/004-tenant-id-column-over-schema-per-tenant.md)). - **DuckDB is proven**: two tenants with identical entity ids resolve to - different rows, cross-tenant lookups 404, aggregates sum only the caller's - rows, and an unscoped read against a shared store is refused — asserted both - by example (`tests/integration/test_tenant_isolation.py`) and over generated - tenant/entity ids (`tests/property/test_tenant_isolation_properties.py`). - **ClickHouse is not**: its adversarial two-tenant suite - (`tests/integration/test_clickhouse_tenant_isolation_live.py`) needs a live - server and has not been run yet. Until it is green, multi-tenant ClickHouse is - not a supported claim; the single-tenant profile is unaffected. +- **Multi-tenant ClickHouse — proven live (audit P0-1).** The boundary is the + `tenant_id` **column**, leading each serving table's write key on both stores + ([ADR-004](decisions/004-tenant-id-column-over-schema-per-tenant.md)). DuckDB + remains covered by example and property suites; ClickHouse is covered by + `tests/integration/test_clickhouse_tenant_isolation_live.py` on live server + 25.3 (CI `test-integration` service + audit Mac stand). Cross-tenant lookups + 404, aggregates stay tenant-scoped, and `assert_tenant_key()` refuses an old + single-column sorting key. Broader isolation across every external dependency + is still out of scope — see [security-audit.md](security-audit.md). - **API RSS growth under steady load — fixed and verified live** (was 175 MB → 1.67 GB over the 4 h soak; the bridge stayed flat). The webhook diff --git a/docs/security-audit.md b/docs/security-audit.md index ae30bdd..5c87545 100644 --- a/docs/security-audit.md +++ b/docs/security-audit.md @@ -55,7 +55,7 @@ Reads pass through one chokepoint. `SQLBuilderMixin._qualify_table` returns a te What the evidence supports today: - **DuckDB** — proven. Two tenants with identical `order_id` resolve to different rows; cross-tenant lookups return `404`; aggregates sum only the caller's rows; an unscoped read fails closed. Property tests assert the invariant over generated tenant and entity ids, not just the two-tenant example. -- **ClickHouse** — the same model is implemented and provisioned (`assert_tenant_key()` refuses to serve a store still on the old sorting key; `provision --migrate` rebuilds one). The adversarial two-tenant suite for it is `tests/integration/test_clickhouse_tenant_isolation_live.py`, which requires a live server and runs on the CI integration job. **Until that suite is green on a real server, treat multi-tenant ClickHouse as unproven and do not claim it.** +- **ClickHouse** — the same model is implemented, provisioned, and proven live. `assert_tenant_key()` refuses to serve a store still on the old sorting key; `provision --migrate` rebuilds one. The adversarial two-tenant suite (`tests/integration/test_clickhouse_tenant_isolation_live.py`) runs against ClickHouse 25.3 on the CI `test-integration` job and was green on the audit stand (full live suite including tenant isolation). Multi-tenant ClickHouse is a supported claim for the write-key + read-chokepoint model described above. It does not support broader claims such as end-to-end isolation across every external dependency. From ae69da20cfa079a957a797333f7bcbd9a8c0319b Mon Sep 17 00:00:00 2001 From: JuliaEdom Date: Thu, 16 Jul 2026 00:50:19 +0300 Subject: [PATCH 2/2] chore(deps): bump schemathesis 4.22.4 and widen pyarrow upper bound Refresh uv.lock so Dependabot PR range bumps pass lock-check: - contract extra: schemathesis 4.22.3 -> 4.22.4 - core: pyarrow >=17,<25 -> >=17,<26 (resolved pin stays 24.0.0) requirements-docker.lock unchanged (contract extra not in docker export; pyarrow resolved version unchanged). --- pyproject.toml | 4 ++-- uv.lock | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02564c1..4822d0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "opentelemetry-sdk>=1.41,<2", "pandera>=0.20,<1", "prometheus-client>=0.21,<1", - "pyarrow>=17,<25", + "pyarrow>=17,<26", "pydantic>=2.9,<3", "pydantic-settings>=2.5,<3", "pyyaml>=6,<7", @@ -73,7 +73,7 @@ integrations = [ "llama-index-core>=0.12,<1", ] contract = [ - "schemathesis==4.22.3", + "schemathesis==4.22.4", ] dev = [ "bandit>=1.9,<2", diff --git a/uv.lock b/uv.lock index 21c0753..d8c9770 100644 --- a/uv.lock +++ b/uv.lock @@ -117,7 +117,7 @@ requires-dist = [ { name = "pandera", specifier = ">=0.20,<1" }, { name = "prometheus-client", specifier = ">=0.21,<1" }, { name = "psycopg", extras = ["binary", "pool"], marker = "extra == 'postgres'", specifier = ">=3.2,<4" }, - { name = "pyarrow", specifier = ">=17,<25" }, + { name = "pyarrow", specifier = ">=17,<26" }, { name = "pydantic", specifier = ">=2.9,<3" }, { name = "pydantic-settings", specifier = ">=2.5,<3" }, { name = "pyiceberg", extras = ["pyiceberg-core"], marker = "extra == 'cloud'", specifier = ">=0.7,<1" }, @@ -126,7 +126,7 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5,<8" }, { name = "pyyaml", specifier = ">=6,<7" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6,<1" }, - { name = "schemathesis", marker = "extra == 'contract'", specifier = "==4.22.3" }, + { name = "schemathesis", marker = "extra == 'contract'", specifier = "==4.22.4" }, { name = "sqlglot", specifier = ">=30,<31" }, { name = "structlog", specifier = ">=24.4,<27" }, { name = "testcontainers", extras = ["kafka"], marker = "extra == 'dev'", specifier = ">=4.9,<5" }, @@ -4637,7 +4637,7 @@ wheels = [ [[package]] name = "schemathesis" -version = "4.22.3" +version = "4.22.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", marker = "python_full_version < '3.14'" }, @@ -4655,9 +4655,9 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.14'" }, { name = "werkzeug", marker = "python_full_version < '3.14'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/50/4ca419862719dde14130595a3654aa16780d225be9074e42d6b90fe3ec7b/schemathesis-4.22.3.tar.gz", hash = "sha256:10c0fb27ed1940030133cf61ba32d1e85f71b85d3570d7ee54d1768b14d64f77", size = 2349247, upload-time = "2026-07-02T08:31:59.656Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/73/ea51c58748d2326a66957b11c116902a77114be25a6bd2cd86880cf9477c/schemathesis-4.22.4.tar.gz", hash = "sha256:1ddaf39e8062a6dc8fc9e14e6e48eac67b879655dc4f5aed725f484c36588437", size = 2349745, upload-time = "2026-07-08T10:50:39.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/13/886a9d15ebaa19dbd98b527781893bab5967152c7fdd5d0f4464eb42a66f/schemathesis-4.22.3-py3-none-any.whl", hash = "sha256:5c99e3375225ba5a299251e11ba955f3875ae7596126318e94d164660e0e7d6a", size = 785083, upload-time = "2026-07-02T08:31:57.843Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/20ac0d9e41a8a43e3fb9a0072088d9cb3dd1edbe23b90d39405bfc18eb67/schemathesis-4.22.4-py3-none-any.whl", hash = "sha256:62ff5d1657e823fc485ac9083e24995f497ae1aba6957b86b310d399317f21cb", size = 785130, upload-time = "2026-07-08T10:50:37.493Z" }, ] [[package]]